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 ". 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) } }