Files
admin 3844df7c23 controllerswap: F1 verify hardening — reject crash-looping no-healthcheck image v0.47.0
controllerHealthy reads RestartCount (running&&rc>0 -> not ok) + signals needsDwell for no-healthcheck;
verify requires verifyDwell(=3) consecutive ok polls for a no-healthcheck image (real healthcheck
trusted immediately). Closes the F1 hole (alpine crash-loop passed the point-in-time check). Red-proof
+ dwell + real-image tests. No sudoers/orchestration change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ
2026-06-29 22:45:41 +02:00

109 lines
3.5 KiB
Go

package localapi
import (
"errors"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
)
// Test for [AGENT-001]: the inline customer-confirmed wipe must re-resolve the
// confirmed durable id to the current device and refuse if anything changed in
// the window, rather than formatting a mutable /dev path that may now point at a
// different physical disk.
func TestAntiRetargetResolve(t *testing.T) {
const durable = "byid:wwn-0xCONFIRMED"
const dev = "/dev/sdb1"
// probe helpers
dataBearing := storage.DeviceProbe{Probed: true, HasFilesystem: true, FSType: "ext4"} // DataBearing() true
blank := storage.DeviceProbe{Probed: true} // Probed, no signatures → not data-bearing
okInspect := func(string) (storage.DeviceProbe, error) { return dataBearing, nil }
cases := []struct {
name string
durable string
resolve func(string) (string, error)
derive func(string) (string, error)
inspect func(string) (storage.DeviceProbe, error)
wantDev string
wantErr string // substring; "" = expect success
}{
{
name: "happy-path-matches",
durable: durable,
resolve: func(string) (string, error) { return dev, nil },
derive: func(string) (string, error) { return durable, nil },
inspect: okInspect,
wantDev: dev,
},
{
name: "empty-durable-refused",
durable: "",
resolve: func(string) (string, error) { return dev, nil },
derive: func(string) (string, error) { return durable, nil },
inspect: okInspect,
wantErr: "path-only",
},
{
name: "durable-no-longer-resolves",
durable: durable,
resolve: func(string) (string, error) { return "", errors.New("gone") },
derive: func(string) (string, error) { return durable, nil },
inspect: okInspect,
wantErr: "no longer resolves",
},
{
// THE core AGENT-001 case: the /dev node now points at a DIFFERENT disk,
// so re-deriving its durable id yields a different value → refuse.
name: "retarget-mismatch-refused",
durable: durable,
resolve: func(string) (string, error) { return dev, nil },
derive: func(string) (string, error) { return "byid:wwn-0xDIFFERENT", nil },
inspect: okInspect,
wantErr: "durable-id mismatch",
},
{
name: "no-longer-data-bearing-refused",
durable: durable,
resolve: func(string) (string, error) { return dev, nil },
derive: func(string) (string, error) { return durable, nil },
inspect: func(string) (storage.DeviceProbe, error) { return blank, nil },
wantErr: "no longer data-bearing",
},
{
name: "reinspect-error-refused",
durable: durable,
resolve: func(string) (string, error) { return dev, nil },
derive: func(string) (string, error) { return durable, nil },
inspect: func(string) (storage.DeviceProbe, error) { return storage.DeviceProbe{}, errors.New("io") },
wantErr: "re-inspect",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := antiRetargetResolve(tc.durable, tc.resolve, tc.derive, tc.inspect)
if tc.wantErr == "" {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tc.wantDev {
t.Fatalf("device = %q, want %q", got, tc.wantDev)
}
return
}
if err == nil {
t.Fatalf("expected refusal containing %q, got device %q and nil error", tc.wantErr, got)
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantErr)
}
if got != "" {
t.Fatalf("on refusal the device must be empty, got %q", got)
}
})
}
}