R-330: the controller decodes SMART 187/188/199 from the agent (carried only, no verdict change)
gates / gates (push) Successful in 56s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-06 22:00:44 +02:00
parent 3d6ba2852f
commit 1de22a166a
3 changed files with 69 additions and 0 deletions
+7
View File
@@ -1065,6 +1065,13 @@ type SmartSummary struct {
ReallocatedSectors *int `json:"reallocated_sectors"`
PendingSectors *int `json:"pending_sectors"`
OfflineUncorrectable *int `json:"offline_uncorrectable"`
// R-330 (agent v0.150.0+): three more SATA raw counters — 187 Reported_Uncorrect, 188
// Command_Timeout (vendor-packed on some drives: carried as reported), 199 UDMA_CRC_Error_Count.
// nil = unknown (an older agent, an NVMe/USB device, or not reported), never zero. CARRIED ONLY:
// no verdict, banner or mail reads them yet — that would change what a household is told.
ReportedUncorrect *int64 `json:"reported_uncorrect,omitempty"`
CommandTimeout *int64 `json:"command_timeout,omitempty"`
UDMACRCErrors *int64 `json:"udma_crc_errors,omitempty"`
// NVMe attributes.
CriticalWarning *int `json:"critical_warning"`
MediaErrors *int `json:"media_errors"`
@@ -0,0 +1,61 @@
package agentapi
import (
"encoding/json"
"reflect"
"testing"
)
// R-330 (disk health Phase 2, the wire only). The agent (v0.150.0+) sends three more SATA raw
// counters; the controller must DECODE them (a field the struct lacks is silently dropped by
// encoding/json) and must not yet ACT on them.
// The 2026-08-14 failing drive as the agent now reports it (values from
// felhom.eu/documentation/audits/fixtures/smart-ST3000VX010-failing-2026-08-14.json).
const r330AgentJSON = `{"health":"PASSED","temperature_c":40,"power_on_hours":null,
"reallocated_sectors":0,"pending_sectors":352,"offline_uncorrectable":352,
"reported_uncorrect":1001,"command_timeout":0,"udma_crc_errors":0,
"critical_warning":null,"media_errors":null,"percentage_used":null}`
// COMPANION RED-PROOF (observed): rename the controller tag `reported_uncorrect` → `reported_uncorrectable`
// → this fails with "187 must decode from the agent's JSON (raw 1001); got <nil>". Restored.
func TestR330_DecodesTheThreeCounters(t *testing.T) {
var s SmartSummary
if err := json.Unmarshal([]byte(r330AgentJSON), &s); err != nil {
t.Fatal(err)
}
if s.ReportedUncorrect == nil || *s.ReportedUncorrect != 1001 {
t.Fatalf("187 must decode from the agent's JSON (raw 1001); got %v", s.ReportedUncorrect)
}
// A measured ZERO stays a zero (not nil): 188 and 199 were reported as 0 by this drive.
if s.CommandTimeout == nil || *s.CommandTimeout != 0 || s.UDMACRCErrors == nil || *s.UDMACRCErrors != 0 {
t.Fatalf("188/199 reported as 0 must decode as a measured 0; got %v %v", s.CommandTimeout, s.UDMACRCErrors)
}
// An older agent omits the keys → nil (unknown), never 0.
var old SmartSummary
if err := json.Unmarshal([]byte(`{"health":"PASSED","pending_sectors":0}`), &old); err != nil {
t.Fatal(err)
}
if old.ReportedUncorrect != nil || old.CommandTimeout != nil || old.UDMACRCErrors != nil {
t.Fatalf("an older agent's report must leave the counters nil (unknown); got %v %v %v",
old.ReportedUncorrect, old.CommandTimeout, old.UDMACRCErrors)
}
}
// CARRIED ONLY: tonight the counters change no verdict, label or degraded-attribute list — reading
// them is a change to what a household is told and is not taken here (R-330's next slice).
func TestR330_CountersChangeNoVerdictYet(t *testing.T) {
with := realDrive()
big := int64(5000)
with.ReportedUncorrect, with.CommandTimeout, with.UDMACRCErrors = &big, &big, &big
without := realDrive()
for _, prior := range []DiskPrior{{}, {SawUncorrectable: true}} {
if a, b := DiskVerdictFor(with, prior), DiskVerdictFor(without, prior); a != b {
t.Fatalf("prior %+v: the R-330 counters changed the verdict (%v vs %v) — that is a household-facing change", prior, a, b)
}
}
if a, b := DegradedAttributes(with), DegradedAttributes(without); !reflect.DeepEqual(a, b) {
t.Fatalf("the R-330 counters changed the degraded-attribute list: %v vs %v", a, b)
}
}