R-330: the hub models SMART 187/188/199 in the host-report (G-1; nothing alarms on them yet)

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:03:22 +02:00
parent ef8f322bda
commit b3c835625d
3 changed files with 50 additions and 3 deletions
+8 -3
View File
@@ -793,9 +793,14 @@ type hostStorageTarget struct {
ReallocatedSectors *int `json:"reallocated_sectors"`
PendingSectors *int `json:"pending_sectors"`
OfflineUncorrectable *int `json:"offline_uncorrectable"`
CriticalWarning *int `json:"critical_warning"`
MediaErrors *int `json:"media_errors"`
PercentageUsed *int `json:"percentage_used"`
// R-330 (agent v0.150.0+): SATA 187/188/199 raw counters; absent (older agent, NVMe/USB, not
// reported) = nil = unknown. Accepted and stored with the report; nothing alarms on them yet.
ReportedUncorrect *int64 `json:"reported_uncorrect,omitempty"`
CommandTimeout *int64 `json:"command_timeout,omitempty"`
UDMACRCErrors *int64 `json:"udma_crc_errors,omitempty"`
CriticalWarning *int `json:"critical_warning"`
MediaErrors *int `json:"media_errors"`
PercentageUsed *int `json:"percentage_used"`
} `json:"smart"`
}
@@ -0,0 +1,41 @@
package api
import (
"encoding/json"
"strings"
"testing"
)
// R-330 (disk health Phase 2, the wire only). Agent v0.150.0+ sends SMART 187/188/199 in
// storage_targets[].smart; the hub's mirror must model them (G-1, wire_contract_gate.py) and keep an
// absent counter absent (unknown), never 0. Nothing on the hub alarms on them yet.
//
// COMPANION RED-PROOF (observed): with the agent branch emitting the tags and the hub mirror lacking
// them, `python3 scripts/wire_contract_gate.py` convicted all three
// (`documentation/audits/night-burndown-2026-10-06/r330/red-hub-wire-gate.txt`); removing the
// `ReportedUncorrect` field makes this test fail with "187 must round-trip …". Restored.
func TestHostStorageTarget_R330_SmartCountersRoundTrip(t *testing.T) {
const withCounters = `{"id":"hdd","smart":{"health":"PASSED","pending_sectors":352,
"reported_uncorrect":1001,"command_timeout":0,"udma_crc_errors":0}}`
var st hostStorageTarget
if err := json.Unmarshal([]byte(withCounters), &st); err != nil {
t.Fatal(err)
}
if st.Smart.ReportedUncorrect == nil || *st.Smart.ReportedUncorrect != 1001 {
t.Fatalf("187 must round-trip through the hub mirror (raw 1001); got %v", st.Smart.ReportedUncorrect)
}
if st.Smart.CommandTimeout == nil || *st.Smart.CommandTimeout != 0 || st.Smart.UDMACRCErrors == nil {
t.Fatalf("a measured 0 must stay a measured 0; got %v %v", st.Smart.CommandTimeout, st.Smart.UDMACRCErrors)
}
var old hostStorageTarget
if err := json.Unmarshal([]byte(`{"id":"hdd","smart":{"health":"PASSED"}}`), &old); err != nil {
t.Fatal(err)
}
b, _ := json.Marshal(old)
for _, k := range []string{"reported_uncorrect", "command_timeout", "udma_crc_errors"} {
if strings.Contains(string(b), `"`+k+`"`) {
t.Fatalf("an older agent's report must not grow a %q key (unknown is absent, not 0): %s", k, b)
}
}
}