fix: surface CPU thermal throttling in status and SAT results

A CPU that had thermally throttled (sysfs thermal_throttle counter >
0) still reported status "OK" everywhere: dmidecode-derived CPU status
only distinguishes populated/enabled/disabled and never looked at the
throttle flag the collector already recorded next to it, and neither
SAT path meant to catch this actually could:

- The routine "cpu" SAT pack (RunCPUAcceptancePack) only checked
  lscpu/sensors/stress-ng exit codes — stress-ng exits 0 whether or
  not the CPU throttled while running it, so an 89°C/throttled CPU
  right after a "successful" run still showed cpu:all as OK in
  component-status.json.
- The more thorough platform-stress test already detected throttling
  and fan-spindown correctly, but wrote its verdict as "Overall: FAIL
  — ..." with no "=", which parseSATKV can't parse — so even a real
  detected throttle event never reached the component-status DB.

Fixes:
- cpu_telemetry.go: escalate a CPU's status to Warning (only-escalate,
  same severity ranking already used elsewhere) when Throttled is set.
- sat.go: add a before/after thermal-throttle-counter check job around
  the "cpu" pack's stress-ng run, so a throttle event during the run
  fails that job and (via the existing FAILED->Warning DB mapping)
  flips cpu:all to Warning.
- platform_stress.go: emit a machine-readable overall_status= line
  alongside the human-readable verdict so platform-stress results
  actually reach ApplySATResultToDB.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-24 17:41:40 +03:00
co-authored by Claude Sonnet 5
parent 56d12b1f3c
commit 198567dffe
6 changed files with 264 additions and 6 deletions
@@ -2,6 +2,7 @@ package platform
import (
"runtime"
"strings"
"testing"
)
@@ -32,3 +33,52 @@ func TestPlatformStressMemoryMBOverride(t *testing.T) {
t.Fatalf("platformStressMemoryMB=%d want 8192", got)
}
}
// TestWritePlatformSummaryOverallStatusKV verifies writePlatformSummary emits
// a parseable "overall_status=" line matching its human-readable "Overall:"
// verdict. ApplySATResultToDB/parseSATKV (internal/app/component_status_db.go)
// only recognizes "key=value" lines — a summary with only "Overall: FAIL —
// ..." (no "=") silently never reaches component-status.json, so a real
// thermal-throttle FAIL detected by this test would never show up as cpu:all
// going Warning.
func TestWritePlatformSummaryOverallStatusKV(t *testing.T) {
opts := PlatformStressOptions{Cycles: []PlatformStressCycle{{LoadSec: 60, IdleSec: 30}}}
tests := []struct {
name string
analyses []cycleAnalysis
want string
}{
{
name: "clean run reports OK",
analyses: []cycleAnalysis{{maxCPUTemp: 70, maxGPUTemp: 60}},
want: "overall_status=OK",
},
{
name: "throttle detected reports FAILED",
analyses: []cycleAnalysis{{maxCPUTemp: 95, throttled: true}},
want: "overall_status=FAILED",
},
{
name: "fast fan spindown reports FAILED",
analyses: []cycleAnalysis{{fanAtCutAvg: 8000, fanMin15s: 2000, fanDropPct: 75}},
want: "overall_status=FAILED",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
summary := writePlatformSummary(opts, tt.analyses)
found := false
for _, line := range strings.Split(summary, "\n") {
if line == tt.want {
found = true
break
}
}
if !found {
t.Fatalf("summary missing %q:\n%s", tt.want, summary)
}
})
}
}