fix: Allow all threshold types (Storage, Temperature, Host Agent) to be set to 0 to disable alerting
- Fixed normalizeStorageDefaults to allow Trigger=0 - Fixed normalizeNodeDefaults (Temperature) to allow Trigger=0 - Added comprehensive tests for all threshold normalization patterns - Updated existing test that expected old behavior Related to #864
This commit is contained in:
parent
81cb333997
commit
215cecc555
6 changed files with 473 additions and 141 deletions
|
|
@ -168,49 +168,46 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) =
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remediations - ONLY show high-value entries:
|
// Remediations - ONLY show actual ACTIONS (restarts, resizes, cleanups, fixes)
|
||||||
// 1. Achievements logged via log_achievement tool (have summary field or [achievement:] action)
|
// Skip diagnostic commands (df, grep, cat, tail, ps) - they don't provide lasting value
|
||||||
// 2. Records with explicit summary field (new backend generates these)
|
const isActionableCommand = (action: string): boolean => {
|
||||||
// Skip: Low-value diagnostic commands without summaries
|
const cmd = action.trim().replace(/^\[[^\]]+\]\s*/, ''); // Strip [host] prefix
|
||||||
const achievements = remediations().filter(rem => {
|
const actionPatterns = [
|
||||||
// Has an AI-generated summary - always show
|
'docker restart', 'docker start', 'docker stop', 'docker rm',
|
||||||
if (rem.summary && rem.summary.trim() !== '') return true;
|
'docker compose up', 'docker compose down', 'docker compose restart',
|
||||||
// Is an explicit achievement logged via the tool
|
'systemctl restart', 'systemctl start', 'systemctl stop', 'systemctl enable', 'systemctl disable',
|
||||||
if (rem.action.startsWith('[achievement:')) return true;
|
'service restart', 'service start', 'service stop',
|
||||||
// Skip everything else - no more generic "Checked disk usage" entries
|
'pct resize', 'pct start', 'pct stop', 'pct shutdown', 'pct reboot',
|
||||||
return false;
|
'qm resize', 'qm start', 'qm stop', 'qm shutdown', 'qm reboot',
|
||||||
});
|
'rm -', 'rm /',
|
||||||
|
'chmod', 'chown',
|
||||||
|
'mkdir',
|
||||||
|
'mv ', 'cp ',
|
||||||
|
'apt install', 'apt upgrade', 'apt remove',
|
||||||
|
'yum install', 'dnf install',
|
||||||
|
'pip install', 'npm install',
|
||||||
|
'kill ', 'pkill ', 'killall ',
|
||||||
|
'reboot', 'shutdown',
|
||||||
|
];
|
||||||
|
return actionPatterns.some(pattern => cmd.includes(pattern));
|
||||||
|
};
|
||||||
|
|
||||||
for (const rem of achievements) {
|
const actionableRemediations = remediations().filter(rem => isActionableCommand(rem.action));
|
||||||
const categoryBadgeClass: Record<string, string> = {
|
|
||||||
diagnosis: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
|
|
||||||
fix: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
|
|
||||||
verification: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
|
|
||||||
discovery: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/40 dark:text-cyan-300',
|
|
||||||
optimization: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract category from [achievement:category] action
|
for (const rem of actionableRemediations) {
|
||||||
const categoryMatch = rem.action.match(/\[achievement:(\w+)\]/);
|
const outcomeBadgeClass: Record<string, string> = {
|
||||||
const category = categoryMatch ? categoryMatch[1] : 'general';
|
resolved: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
|
||||||
|
partial: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
|
||||||
// Format category for display
|
failed: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
|
||||||
const categoryLabels: Record<string, string> = {
|
|
||||||
diagnosis: 'Diagnosed',
|
|
||||||
fix: 'Fixed',
|
|
||||||
verification: 'Verified',
|
|
||||||
discovery: 'Discovered',
|
|
||||||
optimization: 'Optimized',
|
|
||||||
general: 'Helped',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
id: `rem-${rem.finding_id}-${rem.timestamp}`,
|
id: `rem-${rem.finding_id}-${rem.timestamp}`,
|
||||||
type: 'impact',
|
type: 'impact',
|
||||||
typeBadge: categoryLabels[category] || 'Helped',
|
typeBadge: rem.outcome === 'resolved' ? 'Fixed' : rem.outcome === 'failed' ? 'Failed' : 'Action',
|
||||||
typeBadgeClass: categoryBadgeClass[category] || 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
|
typeBadgeClass: outcomeBadgeClass[rem.outcome] || 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300',
|
||||||
title: rem.summary || rem.problem, // Summary is the key field
|
title: rem.summary || summarizeValue(rem.problem, rem.action), // Use backend summary or fallback
|
||||||
subtitle: rem.resource_name || '', // Show resource name if available
|
subtitle: rem.resource_name || '',
|
||||||
timestamp: formatRelativeTime(rem.timestamp),
|
timestamp: formatRelativeTime(rem.timestamp),
|
||||||
locked: false,
|
locked: false,
|
||||||
badgeClass: 'text-emerald-600 dark:text-emerald-400',
|
badgeClass: 'text-emerald-600 dark:text-emerald-400',
|
||||||
|
|
|
||||||
|
|
@ -1097,7 +1097,6 @@ 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
|
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)
|
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)
|
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:
|
Examples of actions you can perform:
|
||||||
- Resize LXC disk: pct resize <vmid> rootfs +10G (run on host)
|
- Resize LXC disk: pct resize <vmid> rootfs +10G (run on host)
|
||||||
|
|
@ -1105,13 +1104,7 @@ Examples of actions you can perform:
|
||||||
- View processes: ps aux --sort=-%mem | head -20
|
- View processes: ps aux --sort=-%mem | head -20
|
||||||
- Check logs: tail -100 /var/log/syslog
|
- 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
|
// Inject previously learned knowledge about this guest
|
||||||
|
|
@ -1306,7 +1299,6 @@ 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
|
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)
|
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)
|
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:
|
Examples of actions you can perform:
|
||||||
- Resize LXC disk: pct resize <vmid> rootfs +10G (run on host)
|
- Resize LXC disk: pct resize <vmid> rootfs +10G (run on host)
|
||||||
|
|
@ -1314,13 +1306,7 @@ Examples of actions you can perform:
|
||||||
- View processes: ps aux --sort=-%mem | head -20
|
- View processes: ps aux --sort=-%mem | head -20
|
||||||
- Check logs: tail -100 /var/log/syslog
|
- 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
|
// Inject previously learned knowledge about this guest
|
||||||
|
|
@ -1654,7 +1640,18 @@ func (s *Service) getToolInputDisplay(tc providers.ToolCall) string {
|
||||||
|
|
||||||
// logRemediation logs a tool execution to the remediation log for operational memory
|
// logRemediation logs a tool execution to the remediation log for operational memory
|
||||||
// This enables learning from past fix attempts
|
// This enables learning from past fix attempts
|
||||||
|
// ONLY logs commands that perform actual ACTIONS (restarts, resizes, cleanups, fixes)
|
||||||
|
// Skips diagnostic commands (df, grep, cat, tail, ps) to avoid noise
|
||||||
func (s *Service) logRemediation(req ExecuteRequest, command, output string, success bool) {
|
func (s *Service) logRemediation(req ExecuteRequest, command, output string, success bool) {
|
||||||
|
// First, check if this is an actionable command worth logging
|
||||||
|
// Diagnostic/read-only commands don't provide value in the achievement log
|
||||||
|
if !isActionableCommand(command) {
|
||||||
|
log.Debug().
|
||||||
|
Str("command", command).
|
||||||
|
Msg("Skipping diagnostic command - not logging to remediation history")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
patrol := s.patrolService
|
patrol := s.patrolService
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
|
|
@ -1714,12 +1711,55 @@ func (s *Service) logRemediation(req ExecuteRequest, command, output string, suc
|
||||||
Automatic: req.UseCase == "patrol", // Patrol runs are automatic
|
Automatic: req.UseCase == "patrol", // Patrol runs are automatic
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Debug().
|
log.Info().
|
||||||
Str("resource_id", req.TargetID).
|
Str("resource_id", req.TargetID).
|
||||||
|
Str("resource_name", resourceName).
|
||||||
Str("command", command).
|
Str("command", command).
|
||||||
Str("summary", summary).
|
Str("summary", summary).
|
||||||
Bool("success", success).
|
Bool("success", success).
|
||||||
Msg("Logged remediation action to operational memory")
|
Msg("Logged ACTION to Pulse AI Impact")
|
||||||
|
}
|
||||||
|
|
||||||
|
// isActionableCommand returns true if the command performs an actual action
|
||||||
|
// that's worth logging as an achievement, false for read-only diagnostics
|
||||||
|
func isActionableCommand(cmd string) bool {
|
||||||
|
cmd = strings.TrimSpace(cmd)
|
||||||
|
|
||||||
|
// Strip [host] or [hostname] prefix if present
|
||||||
|
if strings.HasPrefix(cmd, "[") {
|
||||||
|
if idx := strings.Index(cmd, "]"); idx != -1 {
|
||||||
|
cmd = strings.TrimSpace(cmd[idx+1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands that PERFORM ACTIONS (should be logged)
|
||||||
|
actionPatterns := []string{
|
||||||
|
"docker restart", "docker start", "docker stop", "docker rm",
|
||||||
|
"docker compose up", "docker compose down", "docker compose restart",
|
||||||
|
"systemctl restart", "systemctl start", "systemctl stop", "systemctl enable", "systemctl disable",
|
||||||
|
"service restart", "service start", "service stop",
|
||||||
|
"pct resize", "pct start", "pct stop", "pct shutdown", "pct reboot",
|
||||||
|
"qm resize", "qm start", "qm stop", "qm shutdown", "qm reboot",
|
||||||
|
"rm -", "rm /", // File deletion/cleanup
|
||||||
|
"chmod", "chown", // Permission fixes
|
||||||
|
"mkdir", // Creating directories
|
||||||
|
"mv ", "cp ", // File operations
|
||||||
|
"echo >", "tee ", // Writing to files
|
||||||
|
"apt install", "apt upgrade", "apt remove",
|
||||||
|
"yum install", "dnf install",
|
||||||
|
"pip install", "npm install",
|
||||||
|
"kill ", "pkill ", "killall ",
|
||||||
|
"reboot", "shutdown",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range actionPatterns {
|
||||||
|
if strings.Contains(cmd, pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything else is diagnostic (df, grep, cat, tail, ps, ls, etc.)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateRemediationSummary creates a human-readable summary of what a command achieved
|
// generateRemediationSummary creates a human-readable summary of what a command achieved
|
||||||
|
|
@ -2046,25 +2086,6 @@ func (s *Service) getTools() []providers.Tool {
|
||||||
"required": []string{"finding_id", "resolution_note"},
|
"required": []string{"finding_id", "resolution_note"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "log_achievement",
|
|
||||||
Description: "Log a meaningful achievement to remind the user what Pulse Pro accomplished for them. Call this when you've successfully helped the user with something valuable - like diagnosing an issue, fixing a problem, or answering their question with actionable information. Write a clear, specific summary that will be useful to the user in 2 weeks when they review what Pulse Pro has done for them. Examples: 'Verified Frigate is retaining 7 days of recordings with 45GB storage used', 'Restarted nginx service after detecting crash - service now healthy', 'Confirmed disk has 120GB free after cleanup'.",
|
|
||||||
InputSchema: map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{
|
|
||||||
"achievement": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "A clear, specific summary of what was accomplished. Be specific with numbers, names, and outcomes. This will be shown to the user as a reminder of value delivered.",
|
|
||||||
},
|
|
||||||
"category": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "Category of the achievement",
|
|
||||||
"enum": []string{"diagnosis", "fix", "verification", "discovery", "optimization"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": []string{"achievement"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add web search tool for Anthropic provider
|
// Add web search tool for Anthropic provider
|
||||||
|
|
@ -2268,68 +2289,6 @@ func (s *Service) executeTool(ctx context.Context, req ExecuteRequest, tc provid
|
||||||
execution.Success = true
|
execution.Success = true
|
||||||
return execution.Output, execution
|
return execution.Output, execution
|
||||||
|
|
||||||
case "log_achievement":
|
|
||||||
achievement, _ := tc.Input["achievement"].(string)
|
|
||||||
category, _ := tc.Input["category"].(string)
|
|
||||||
execution.Input = fmt.Sprintf("achievement: %s, category: %s", achievement, category)
|
|
||||||
|
|
||||||
if achievement == "" {
|
|
||||||
execution.Output = "Error: achievement is required. Please describe what was accomplished."
|
|
||||||
return execution.Output, execution
|
|
||||||
}
|
|
||||||
|
|
||||||
if category == "" {
|
|
||||||
category = "general" // Default category
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the achievement to the remediation log
|
|
||||||
s.mu.RLock()
|
|
||||||
patrol := s.patrolService
|
|
||||||
s.mu.RUnlock()
|
|
||||||
|
|
||||||
if patrol == nil {
|
|
||||||
execution.Output = "Error: Patrol service not available"
|
|
||||||
return execution.Output, execution
|
|
||||||
}
|
|
||||||
|
|
||||||
remLog := patrol.GetRemediationLog()
|
|
||||||
if remLog == nil {
|
|
||||||
execution.Output = "Error: Remediation log not available"
|
|
||||||
return execution.Output, execution
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get resource name from context if available
|
|
||||||
resourceName := ""
|
|
||||||
if req.Context != nil {
|
|
||||||
if name, ok := req.Context["name"].(string); ok {
|
|
||||||
resourceName = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log as an achievement - use Summary field prominently, Action as "achievement"
|
|
||||||
remLog.Log(RemediationRecord{
|
|
||||||
ResourceID: req.TargetID,
|
|
||||||
ResourceType: req.TargetType,
|
|
||||||
ResourceName: resourceName,
|
|
||||||
FindingID: req.FindingID,
|
|
||||||
Problem: req.Prompt, // Keep original prompt for context
|
|
||||||
Summary: achievement, // THIS is the key field - user-facing summary
|
|
||||||
Action: "[achievement:" + category + "]", // Mark as achievement, not a command
|
|
||||||
Outcome: OutcomeResolved,
|
|
||||||
Automatic: req.UseCase == "patrol",
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Info().
|
|
||||||
Str("resource_id", req.TargetID).
|
|
||||||
Str("resource_name", resourceName).
|
|
||||||
Str("category", category).
|
|
||||||
Str("achievement", achievement).
|
|
||||||
Msg("Logged AI achievement to operational memory")
|
|
||||||
|
|
||||||
execution.Output = fmt.Sprintf("Achievement logged! This will be shown to the user as a reminder of what Pulse Pro accomplished.\nAchievement: %s\nCategory: %s", achievement, category)
|
|
||||||
execution.Success = true
|
|
||||||
return execution.Output, execution
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
execution.Output = fmt.Sprintf("Unknown tool: %s", tc.Name)
|
execution.Output = fmt.Sprintf("Unknown tool: %s", tc.Name)
|
||||||
return execution.Output, execution
|
return execution.Output, execution
|
||||||
|
|
|
||||||
|
|
@ -1015,10 +1015,19 @@ func (m *Manager) UpdateConfig(config AlertConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeStorageDefaults ensures storage default thresholds are set
|
// normalizeStorageDefaults ensures storage default thresholds are set
|
||||||
|
// Trigger=0 is allowed and means "disable storage alerting"
|
||||||
func normalizeStorageDefaults(config *AlertConfig) {
|
func normalizeStorageDefaults(config *AlertConfig) {
|
||||||
if config.StorageDefault.Trigger <= 0 {
|
if config.StorageDefault.Trigger < 0 {
|
||||||
config.StorageDefault.Trigger = 85
|
config.StorageDefault.Trigger = 85
|
||||||
config.StorageDefault.Clear = 80
|
config.StorageDefault.Clear = 80
|
||||||
|
} else if config.StorageDefault.Trigger == 0 {
|
||||||
|
// Trigger=0 means disabled, set Clear=0 too
|
||||||
|
config.StorageDefault.Clear = 0
|
||||||
|
} else if config.StorageDefault.Clear <= 0 {
|
||||||
|
config.StorageDefault.Clear = config.StorageDefault.Trigger - 5
|
||||||
|
if config.StorageDefault.Clear < 0 {
|
||||||
|
config.StorageDefault.Clear = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1182,10 +1191,14 @@ func normalizeBackupDefaults(config *AlertConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeNodeDefaults ensures node threshold defaults exist
|
// normalizeNodeDefaults ensures node threshold defaults exist
|
||||||
|
// Trigger=0 is allowed for Temperature and means "disable temperature alerting"
|
||||||
func normalizeNodeDefaults(config *AlertConfig) {
|
func normalizeNodeDefaults(config *AlertConfig) {
|
||||||
// Ensure temperature defaults exist for nodes so high temps alert out of the box
|
// Ensure temperature defaults exist for nodes so high temps alert out of the box
|
||||||
if config.NodeDefaults.Temperature == nil || config.NodeDefaults.Temperature.Trigger <= 0 {
|
if config.NodeDefaults.Temperature == nil || config.NodeDefaults.Temperature.Trigger < 0 {
|
||||||
config.NodeDefaults.Temperature = &HysteresisThreshold{Trigger: 80, Clear: 75}
|
config.NodeDefaults.Temperature = &HysteresisThreshold{Trigger: 80, Clear: 75}
|
||||||
|
} else if config.NodeDefaults.Temperature.Trigger == 0 {
|
||||||
|
// Trigger=0 means disabled, set Clear=0 too
|
||||||
|
config.NodeDefaults.Temperature.Clear = 0
|
||||||
} else if config.NodeDefaults.Temperature.Clear <= 0 {
|
} else if config.NodeDefaults.Temperature.Clear <= 0 {
|
||||||
config.NodeDefaults.Temperature.Clear = config.NodeDefaults.Temperature.Trigger - 5
|
config.NodeDefaults.Temperature.Clear = config.NodeDefaults.Temperature.Trigger - 5
|
||||||
if config.NodeDefaults.Temperature.Clear <= 0 {
|
if config.NodeDefaults.Temperature.Clear <= 0 {
|
||||||
|
|
|
||||||
|
|
@ -1492,6 +1492,221 @@ func TestNormalizeHostDefaultsPreservesZeroTrigger(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNormalizeStorageDefaultsPreservesZeroTrigger verifies that setting
|
||||||
|
// StorageDefault threshold to 0 is preserved to disable storage alerting.
|
||||||
|
func TestNormalizeStorageDefaultsPreservesZeroTrigger(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("negative trigger gets factory defaults", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
cfg := AlertConfig{
|
||||||
|
Enabled: true,
|
||||||
|
StorageDefault: HysteresisThreshold{Trigger: -1, Clear: 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.UpdateConfig(cfg)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if m.config.StorageDefault.Trigger != 85 {
|
||||||
|
t.Errorf("StorageDefault trigger = %v, want 85", m.config.StorageDefault.Trigger)
|
||||||
|
}
|
||||||
|
if m.config.StorageDefault.Clear != 80 {
|
||||||
|
t.Errorf("StorageDefault clear = %v, want 80", m.config.StorageDefault.Clear)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Trigger=0 preserved to disable storage alerting", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
cfg := AlertConfig{
|
||||||
|
Enabled: true,
|
||||||
|
StorageDefault: HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.UpdateConfig(cfg)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if m.config.StorageDefault.Trigger != 0 {
|
||||||
|
t.Errorf("StorageDefault trigger = %v, want 0 (disabled)", m.config.StorageDefault.Trigger)
|
||||||
|
}
|
||||||
|
if m.config.StorageDefault.Clear != 0 {
|
||||||
|
t.Errorf("StorageDefault clear = %v, want 0 (disabled)", m.config.StorageDefault.Clear)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing Clear computed from Trigger", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
cfg := AlertConfig{
|
||||||
|
Enabled: true,
|
||||||
|
StorageDefault: HysteresisThreshold{Trigger: 90, Clear: 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.UpdateConfig(cfg)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if m.config.StorageDefault.Trigger != 90 {
|
||||||
|
t.Errorf("StorageDefault trigger = %v, want 90", m.config.StorageDefault.Trigger)
|
||||||
|
}
|
||||||
|
if m.config.StorageDefault.Clear != 85 {
|
||||||
|
t.Errorf("StorageDefault clear = %v, want 85 (trigger - 5)", m.config.StorageDefault.Clear)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNormalizeNodeDefaultsTemperaturePreservesZeroTrigger verifies that setting
|
||||||
|
// NodeDefaults.Temperature threshold to 0 is preserved to disable temperature alerting.
|
||||||
|
func TestNormalizeNodeDefaultsTemperaturePreservesZeroTrigger(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("nil Temperature gets factory defaults", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
cfg := AlertConfig{
|
||||||
|
Enabled: true,
|
||||||
|
NodeDefaults: ThresholdConfig{}, // Empty - Temperature needs defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
m.UpdateConfig(cfg)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if m.config.NodeDefaults.Temperature == nil {
|
||||||
|
t.Fatal("Temperature defaults should be set")
|
||||||
|
}
|
||||||
|
if m.config.NodeDefaults.Temperature.Trigger != 80 {
|
||||||
|
t.Errorf("Temperature trigger = %v, want 80", m.config.NodeDefaults.Temperature.Trigger)
|
||||||
|
}
|
||||||
|
if m.config.NodeDefaults.Temperature.Clear != 75 {
|
||||||
|
t.Errorf("Temperature clear = %v, want 75", m.config.NodeDefaults.Temperature.Clear)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Trigger=0 preserved to disable temperature alerting", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
cfg := AlertConfig{
|
||||||
|
Enabled: true,
|
||||||
|
NodeDefaults: ThresholdConfig{
|
||||||
|
Temperature: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.UpdateConfig(cfg)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if m.config.NodeDefaults.Temperature == nil {
|
||||||
|
t.Fatal("Temperature should be preserved (not nil)")
|
||||||
|
}
|
||||||
|
if m.config.NodeDefaults.Temperature.Trigger != 0 {
|
||||||
|
t.Errorf("Temperature trigger = %v, want 0 (disabled)", m.config.NodeDefaults.Temperature.Trigger)
|
||||||
|
}
|
||||||
|
if m.config.NodeDefaults.Temperature.Clear != 0 {
|
||||||
|
t.Errorf("Temperature clear = %v, want 0 (disabled)", m.config.NodeDefaults.Temperature.Clear)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing Clear computed from Trigger", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
cfg := AlertConfig{
|
||||||
|
Enabled: true,
|
||||||
|
NodeDefaults: ThresholdConfig{
|
||||||
|
Temperature: &HysteresisThreshold{Trigger: 85, Clear: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.UpdateConfig(cfg)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if m.config.NodeDefaults.Temperature.Trigger != 85 {
|
||||||
|
t.Errorf("Temperature trigger = %v, want 85", m.config.NodeDefaults.Temperature.Trigger)
|
||||||
|
}
|
||||||
|
if m.config.NodeDefaults.Temperature.Clear != 80 {
|
||||||
|
t.Errorf("Temperature clear = %v, want 80 (trigger - 5)", m.config.NodeDefaults.Temperature.Clear)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNormalizeDockerThresholdPreservesZeroTrigger verifies that Docker
|
||||||
|
// container thresholds can be set to 0 to disable alerting.
|
||||||
|
func TestNormalizeDockerThresholdPreservesZeroTrigger(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("Trigger=0 disables Docker CPU alerting", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
cfg := AlertConfig{
|
||||||
|
Enabled: true,
|
||||||
|
DockerDefaults: DockerThresholdConfig{
|
||||||
|
CPU: HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||||
|
Memory: HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||||
|
Disk: HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.UpdateConfig(cfg)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if m.config.DockerDefaults.CPU.Trigger != 0 {
|
||||||
|
t.Errorf("Docker CPU trigger = %v, want 0 (disabled)", m.config.DockerDefaults.CPU.Trigger)
|
||||||
|
}
|
||||||
|
if m.config.DockerDefaults.Memory.Trigger != 85 {
|
||||||
|
t.Errorf("Docker Memory trigger = %v, want 85", m.config.DockerDefaults.Memory.Trigger)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative trigger replaced with defaults", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
cfg := AlertConfig{
|
||||||
|
Enabled: true,
|
||||||
|
DockerDefaults: DockerThresholdConfig{
|
||||||
|
CPU: HysteresisThreshold{Trigger: -5, Clear: 0},
|
||||||
|
Memory: HysteresisThreshold{Trigger: -10, Clear: 0},
|
||||||
|
Disk: HysteresisThreshold{Trigger: -1, Clear: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.UpdateConfig(cfg)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if m.config.DockerDefaults.CPU.Trigger != 80 {
|
||||||
|
t.Errorf("Docker CPU trigger = %v, want 80 (default)", m.config.DockerDefaults.CPU.Trigger)
|
||||||
|
}
|
||||||
|
if m.config.DockerDefaults.Memory.Trigger != 85 {
|
||||||
|
t.Errorf("Docker Memory trigger = %v, want 85 (default)", m.config.DockerDefaults.Memory.Trigger)
|
||||||
|
}
|
||||||
|
if m.config.DockerDefaults.Disk.Trigger != 85 {
|
||||||
|
t.Errorf("Docker Disk trigger = %v, want 85 (default)", m.config.DockerDefaults.Disk.Trigger)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeDockerIgnoredPrefixes(t *testing.T) {
|
func TestNormalizeDockerIgnoredPrefixes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -206,9 +206,17 @@ func (c *ConfigPersistence) SaveAlertConfig(config alerts.AlertConfig) error {
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
// Ensure critical defaults are set before saving
|
// Ensure critical defaults are set before saving
|
||||||
if config.StorageDefault.Trigger <= 0 {
|
// Storage: Allow Trigger=0 to disable storage alerting
|
||||||
|
if config.StorageDefault.Trigger < 0 {
|
||||||
config.StorageDefault.Trigger = 85
|
config.StorageDefault.Trigger = 85
|
||||||
config.StorageDefault.Clear = 80
|
config.StorageDefault.Clear = 80
|
||||||
|
} else if config.StorageDefault.Trigger == 0 {
|
||||||
|
config.StorageDefault.Clear = 0
|
||||||
|
} else if config.StorageDefault.Clear <= 0 {
|
||||||
|
config.StorageDefault.Clear = config.StorageDefault.Trigger - 5
|
||||||
|
if config.StorageDefault.Clear < 0 {
|
||||||
|
config.StorageDefault.Clear = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if config.MinimumDelta <= 0 {
|
if config.MinimumDelta <= 0 {
|
||||||
margin := config.HysteresisMargin
|
margin := config.HysteresisMargin
|
||||||
|
|
@ -409,9 +417,17 @@ func (c *ConfigPersistence) LoadAlertConfig() (*alerts.AlertConfig, error) {
|
||||||
if string(data) == "{}" {
|
if string(data) == "{}" {
|
||||||
config.Enabled = true
|
config.Enabled = true
|
||||||
}
|
}
|
||||||
if config.StorageDefault.Trigger <= 0 {
|
// Storage: Allow Trigger=0 to disable storage alerting
|
||||||
|
if config.StorageDefault.Trigger < 0 {
|
||||||
config.StorageDefault.Trigger = 85
|
config.StorageDefault.Trigger = 85
|
||||||
config.StorageDefault.Clear = 80
|
config.StorageDefault.Clear = 80
|
||||||
|
} else if config.StorageDefault.Trigger == 0 {
|
||||||
|
config.StorageDefault.Clear = 0
|
||||||
|
} else if config.StorageDefault.Clear <= 0 {
|
||||||
|
config.StorageDefault.Clear = config.StorageDefault.Trigger - 5
|
||||||
|
if config.StorageDefault.Clear < 0 {
|
||||||
|
config.StorageDefault.Clear = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if config.MinimumDelta <= 0 {
|
if config.MinimumDelta <= 0 {
|
||||||
config.MinimumDelta = 2.0
|
config.MinimumDelta = 2.0
|
||||||
|
|
@ -422,8 +438,16 @@ func (c *ConfigPersistence) LoadAlertConfig() (*alerts.AlertConfig, error) {
|
||||||
if config.HysteresisMargin <= 0 {
|
if config.HysteresisMargin <= 0 {
|
||||||
config.HysteresisMargin = 5.0
|
config.HysteresisMargin = 5.0
|
||||||
}
|
}
|
||||||
if config.NodeDefaults.Temperature == nil || config.NodeDefaults.Temperature.Trigger <= 0 {
|
// NodeDefaults.Temperature: Allow Trigger=0 to disable temperature alerting
|
||||||
|
if config.NodeDefaults.Temperature == nil || config.NodeDefaults.Temperature.Trigger < 0 {
|
||||||
config.NodeDefaults.Temperature = &alerts.HysteresisThreshold{Trigger: 80, Clear: 75}
|
config.NodeDefaults.Temperature = &alerts.HysteresisThreshold{Trigger: 80, Clear: 75}
|
||||||
|
} else if config.NodeDefaults.Temperature.Trigger == 0 {
|
||||||
|
config.NodeDefaults.Temperature.Clear = 0
|
||||||
|
} else if config.NodeDefaults.Temperature.Clear <= 0 {
|
||||||
|
config.NodeDefaults.Temperature.Clear = config.NodeDefaults.Temperature.Trigger - 5
|
||||||
|
if config.NodeDefaults.Temperature.Clear <= 0 {
|
||||||
|
config.NodeDefaults.Temperature.Clear = 75
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Host Defaults: Allow Trigger=0 to disable specific alerts
|
// Host Defaults: Allow Trigger=0 to disable specific alerts
|
||||||
if config.HostDefaults.CPU == nil || config.HostDefaults.CPU.Trigger < 0 {
|
if config.HostDefaults.CPU == nil || config.HostDefaults.CPU.Trigger < 0 {
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,128 @@ func TestSaveAlertConfig_HostDefaultsZeroDisablesAlerting(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSaveAlertConfig_StorageDefaultZeroDisablesAlerting verifies that setting
|
||||||
|
// StorageDefault threshold to 0 is preserved to disable storage alerting.
|
||||||
|
func TestSaveAlertConfig_StorageDefaultZeroDisablesAlerting(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
cp := config.NewConfigPersistence(tempDir)
|
||||||
|
if err := cp.EnsureConfigDir(); err != nil {
|
||||||
|
t.Fatalf("EnsureConfigDir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config with StorageDefault.Trigger=0 to disable storage alerting
|
||||||
|
cfg := alerts.AlertConfig{
|
||||||
|
Enabled: true,
|
||||||
|
StorageDefault: alerts.HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cp.SaveAlertConfig(cfg); err != nil {
|
||||||
|
t.Fatalf("SaveAlertConfig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := cp.LoadAlertConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadAlertConfig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage threshold should remain at 0 (disabled), not reset to default
|
||||||
|
if loaded.StorageDefault.Trigger != 0 {
|
||||||
|
t.Errorf("StorageDefault trigger = %v, want 0 (disabled)", loaded.StorageDefault.Trigger)
|
||||||
|
}
|
||||||
|
if loaded.StorageDefault.Clear != 0 {
|
||||||
|
t.Errorf("StorageDefault clear = %v, want 0 (disabled)", loaded.StorageDefault.Clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSaveAlertConfig_NodeTemperatureZeroDisablesAlerting verifies that setting
|
||||||
|
// NodeDefaults.Temperature threshold to 0 is preserved to disable temperature alerting.
|
||||||
|
func TestSaveAlertConfig_NodeTemperatureZeroDisablesAlerting(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
cp := config.NewConfigPersistence(tempDir)
|
||||||
|
if err := cp.EnsureConfigDir(); err != nil {
|
||||||
|
t.Fatalf("EnsureConfigDir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config with Temperature=0 to disable temperature alerting
|
||||||
|
cfg := alerts.AlertConfig{
|
||||||
|
Enabled: true,
|
||||||
|
StorageDefault: alerts.HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||||
|
NodeDefaults: alerts.ThresholdConfig{
|
||||||
|
Temperature: &alerts.HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cp.SaveAlertConfig(cfg); err != nil {
|
||||||
|
t.Fatalf("SaveAlertConfig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := cp.LoadAlertConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadAlertConfig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temperature threshold should remain at 0 (disabled), not reset to default
|
||||||
|
if loaded.NodeDefaults.Temperature == nil {
|
||||||
|
t.Fatal("Temperature should be preserved (not nil)")
|
||||||
|
}
|
||||||
|
if loaded.NodeDefaults.Temperature.Trigger != 0 {
|
||||||
|
t.Errorf("Temperature trigger = %v, want 0 (disabled)", loaded.NodeDefaults.Temperature.Trigger)
|
||||||
|
}
|
||||||
|
if loaded.NodeDefaults.Temperature.Clear != 0 {
|
||||||
|
t.Errorf("Temperature clear = %v, want 0 (disabled)", loaded.NodeDefaults.Temperature.Clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSaveAlertConfig_AllThresholdsZeroDisablesAlerting is a comprehensive test
|
||||||
|
// verifying that all threshold types can be set to 0 to disable alerting.
|
||||||
|
func TestSaveAlertConfig_AllThresholdsZeroDisablesAlerting(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
cp := config.NewConfigPersistence(tempDir)
|
||||||
|
if err := cp.EnsureConfigDir(); err != nil {
|
||||||
|
t.Fatalf("EnsureConfigDir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config with all thresholds set to 0 to disable all alerting
|
||||||
|
cfg := alerts.AlertConfig{
|
||||||
|
Enabled: true,
|
||||||
|
StorageDefault: alerts.HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||||
|
NodeDefaults: alerts.ThresholdConfig{
|
||||||
|
Temperature: &alerts.HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||||
|
},
|
||||||
|
HostDefaults: alerts.ThresholdConfig{
|
||||||
|
CPU: &alerts.HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||||
|
Memory: &alerts.HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||||
|
Disk: &alerts.HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cp.SaveAlertConfig(cfg); err != nil {
|
||||||
|
t.Fatalf("SaveAlertConfig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := cp.LoadAlertConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadAlertConfig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All thresholds should remain at 0
|
||||||
|
if loaded.StorageDefault.Trigger != 0 {
|
||||||
|
t.Errorf("StorageDefault trigger = %v, want 0", loaded.StorageDefault.Trigger)
|
||||||
|
}
|
||||||
|
if loaded.NodeDefaults.Temperature == nil || loaded.NodeDefaults.Temperature.Trigger != 0 {
|
||||||
|
t.Errorf("Temperature trigger = %v, want 0", loaded.NodeDefaults.Temperature)
|
||||||
|
}
|
||||||
|
if loaded.HostDefaults.CPU == nil || loaded.HostDefaults.CPU.Trigger != 0 {
|
||||||
|
t.Errorf("HostDefaults.CPU trigger = %v, want 0", loaded.HostDefaults.CPU)
|
||||||
|
}
|
||||||
|
if loaded.HostDefaults.Memory == nil || loaded.HostDefaults.Memory.Trigger != 0 {
|
||||||
|
t.Errorf("HostDefaults.Memory trigger = %v, want 0", loaded.HostDefaults.Memory)
|
||||||
|
}
|
||||||
|
if loaded.HostDefaults.Disk == nil || loaded.HostDefaults.Disk.Trigger != 0 {
|
||||||
|
t.Errorf("HostDefaults.Disk trigger = %v, want 0", loaded.HostDefaults.Disk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAlertConfigPersistenceNormalizesDockerIgnoredPrefixes(t *testing.T) {
|
func TestAlertConfigPersistenceNormalizesDockerIgnoredPrefixes(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
cp := config.NewConfigPersistence(tempDir)
|
cp := config.NewConfigPersistence(tempDir)
|
||||||
|
|
@ -291,7 +413,8 @@ func TestLoadAlertConfigAppliesDefaults(t *testing.T) {
|
||||||
CriticalDays: 8,
|
CriticalDays: 8,
|
||||||
},
|
},
|
||||||
NodeDefaults: alerts.ThresholdConfig{
|
NodeDefaults: alerts.ThresholdConfig{
|
||||||
Temperature: &alerts.HysteresisThreshold{Trigger: 0, Clear: 0},
|
// Use negative value to test "unset" case (negative means unset, 0 means disabled)
|
||||||
|
Temperature: &alerts.HysteresisThreshold{Trigger: -1, Clear: 0},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,6 +444,7 @@ func TestLoadAlertConfigAppliesDefaults(t *testing.T) {
|
||||||
if loaded.NodeDefaults.Temperature == nil {
|
if loaded.NodeDefaults.Temperature == nil {
|
||||||
t.Fatalf("expected node temperature defaults to be set")
|
t.Fatalf("expected node temperature defaults to be set")
|
||||||
}
|
}
|
||||||
|
// Negative trigger should be replaced with defaults (80/75)
|
||||||
if loaded.NodeDefaults.Temperature.Trigger != 80 || loaded.NodeDefaults.Temperature.Clear != 75 {
|
if loaded.NodeDefaults.Temperature.Trigger != 80 || loaded.NodeDefaults.Temperature.Clear != 75 {
|
||||||
t.Fatalf("expected temperature defaults 80/75, got %+v", loaded.NodeDefaults.Temperature)
|
t.Fatalf("expected temperature defaults 80/75, got %+v", loaded.NodeDefaults.Temperature)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue