package localapi import ( "context" "encoding/json" "io" "log/slog" "testing" "gitea.dooplex.hu/admin/felhom-agent/internal/hub" "gitea.dooplex.hu/admin/felhom-agent/internal/storage" ) // R-116 — the backup-target flag must be reachable from the row the CONTROLLER keys on. // // THE DEFECT. The controller resolves the drive-absent alarm by the registered StoragePath, which for // an external drive is the GUEST path. When the device vanishes, Observe's exactMountDevice fails, so // BackingDevice becomes "" and RoleForStorage returns RoleSystem (role.go:180-181) — the guest-path // block is skipped and the flag-bearing row loses its guest path. It keeps its MountPath, so the union // loop DEDUPES the registry row away, and /disks carries NO row with that guest path at all. // driveTargetByPath then has no entry, isTarget[guestPath] is a MISSING KEY, and the specific // backup_target_absent alarm cannot fire — the generic storage_disconnected goes out instead, while // the RETURN (rows rejoined) fires the specific recovery. An operator gets a pair they cannot match. // Measured live: felhom.eu audits/SESSION-C-2026-07-29.md §5. // // These tests exercise the REAL GET /disks response and assert the emitted JSON, because the failure // class is "the value is on the wrong row" — a test that hand-builds rows proves nothing about which // row the handler actually emits. // targetRowServer builds a /disks server whose primary backup tier is `primaryTarget`, over the given // Observe targets. boundCheck/deviceCheck are pinned so the R-113 conjunction is not the variable // under test here. func targetRowServer(t *testing.T, primaryTarget string, targets []hub.StorageTarget) *Server { t.Helper() srv, err := NewServer(Options{ ListenAddr: "127.0.0.1:0", Guests: &fakeGuestsCfg{}, Backups: &fakeBackups{}, Store: &fakeStore{}, Storage: fakeStorage{targets: targets}, // Service is REQUIRED: normalizeBackupTiers (backup_tiers.go:21-22) drops any tier with a nil // Service, and the legacy fallback then yields TargetID "" — which silently makes every // BackupTarget false and would make these tests pass for the wrong reason. BackupTiers: []BackupTier{{TargetID: primaryTarget, Primary: true, Service: &fakeBackups{}}}, Tokens: staticTokens{"A": 8200}, Disks: &fakeDiskOps{probe: storage.DeviceProbe{Probed: true, HasFilesystem: true, FSType: "ext4"}}, DiskGate: &fakeGate{}, HostReader: sysOnSDA(), Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), }) if err != nil { t.Fatal(err) } srv.baseCtx = context.Background() srv.boundCheck = func(string) bool { return true } srv.deviceCheck = func(string) bool { return true } return srv } // wireDisks returns the decoded /disks rows exactly as the controller receives them. func wireDisks(t *testing.T, srv *Server) []map[string]any { t.Helper() body := do(t, srv.Handler(), "GET", "/disks", "A", "").Body.Bytes() var w struct { Data struct { Disks []map[string]any `json:"disks"` } `json:"data"` } if err := json.Unmarshal(body, &w); err != nil { t.Fatalf("decode /disks: %v (%s)", err, body) } return w.Data.Disks } // isTargetByPath reproduces the controller's driveTargetByPath EXACTLY (intermediary.go:602-616): // both keyings, value = backup_target. This is the map whose missing key is the whole defect, so the // assertion is made against a faithful copy of it rather than against a field in isolation. func isTargetByPath(disks []map[string]any) map[string]bool { out := map[string]bool{} for _, d := range disks { bt, _ := d["backup_target"].(bool) if gp, ok := d["guest_path"].(string); ok && gp != "" { out[gp] = bt } if mp, ok := d["mount_path"].(string); ok && mp != "" { out[mp] = bt } } return out } // theAbsentTarget is the Session-C shape: the felhom-backup storage whose device has gone, so Observe // reports no backing device — which is what flips its role to system and drops its guest path. var theAbsentTarget = hub.StorageTarget{ Name: "felhom-backup", Type: hub.StorageTypeLocalDir, MountPath: "/mnt/mentes", BackingDevice: "", State: hub.StorageStateDisconnected, } // ── the observable that must move ─────────────────────────────────────────────────────────────── // RED-PROOF: delete the `di.GuestPath == "" && di.BackupTarget && t.BackingDevice == ""` block and // this fails with "the guest path the controller keys on is MISSING from /disks entirely". func TestAbsentBackupTargetIsResolvableByGuestPath(t *testing.T) { disks := wireDisks(t, targetRowServer(t, "felhom-backup", []hub.StorageTarget{theAbsentTarget})) isTarget := isTargetByPath(disks) const guestPath = "/mnt/felhom-drives/mentes" got, present := isTarget[guestPath] if !present { t.Fatalf("isTarget[%q] is a MISSING KEY — the guest path the controller keys on is missing from "+ "/disks entirely, so notifyDriveAbsent takes the generic branch and backup_target_absent "+ "can never fire (R-116)", guestPath) } if !got { t.Errorf("isTarget[%q] = false; the row carrying the guest path does not carry the flag", guestPath) } // The host-path key was never the broken one — it must stay true. if !isTarget["/mnt/mentes"] { t.Error("isTarget by host path regressed to false") } } // ── V2: the new guest path must NOT make the gate read the drive as PRESENT ───────────────────── // This is the over-correction guard, in the exact component under test. planDriveGates computes // present[gp] = present[gp] || d.BoundUnderParent. If the row we now emit carried a true // BoundUnderParent, this fix would SILENCE the alarm it exists to raise. func TestAbsentTargetRowDoesNotRegisterPresence(t *testing.T) { srv := targetRowServer(t, "felhom-backup", []hub.StorageTarget{theAbsentTarget}) // deviceCheck/boundCheck are pinned TRUE — the strongest possible case for a false positive. // The row must still report bound_under_parent=false, because that field is only ever assigned // inside the guest-path blocks a system-role row does not enter. for _, d := range wireDisks(t, srv) { if d["guest_path"] != "/mnt/felhom-drives/mentes" { continue } if bup, _ := d["bound_under_parent"].(bool); bup { t.Fatal("the absent backup-target row reports bound_under_parent=true — planDriveGates " + "would compute present=true, the Stop branch would never run, and this fix would " + "SUPPRESS the very alarm it exists to raise") } return } t.Fatal("the absent target row never reached the wire") } // ── V1: the gates, each on its own ────────────────────────────────────────────────────────────── // Case B is the COMMON fresh-box shape, not an edge: the tier target is the builtin `local` on the // root fs. It must never acquire a guest path. func TestCaseBLocalTargetGetsNoGuestPath(t *testing.T) { disks := wireDisks(t, targetRowServer(t, "local", []hub.StorageTarget{ {Name: "local", Type: "local", MountPath: "/var/lib/vz", BackingDevice: "", State: hub.StorageStateAttached}, })) for _, d := range disks { if gp, _ := d["guest_path"].(string); gp != "" { t.Errorf("the Case B target on %v acquired guest path %q — a system-drive backup target "+ "must not cross into the guest", d["mount_path"], gp) } } } // A storage that is RoleSystem because it is genuinely system-BACKED (non-empty BackingDevice on the // system disk) must be excluded — this is the case StablePathForRaw would NOT have filtered, since // /mnt/ maps to a real stable path. The BackingDevice gate is what stops it. func TestSystemBackedTargetUnderMntGetsNoGuestPath(t *testing.T) { disks := wireDisks(t, targetRowServer(t, "sysbackup", []hub.StorageTarget{ // sysOnSDA() makes /dev/sda the system disk, so this classifies RoleSystem with a REAL device. {Name: "sysbackup", Type: hub.StorageTypeLocalDir, MountPath: "/mnt/sysbackup", BackingDevice: "/dev/sda1", State: hub.StorageStateAttached}, })) for _, d := range disks { if gp, _ := d["guest_path"].(string); gp != "" { t.Errorf("a system-BACKED backup target acquired guest path %q — the BackingDevice gate "+ "failed and the :213-214 boundary was widened", gp) } } } // ── the negative ──────────────────────────────────────────────────────────────────────────────── // A drive that is NOT the target must not acquire the flag on any row, present or absent. func TestNonTargetDriveNeverCarriesTheFlag(t *testing.T) { disks := wireDisks(t, targetRowServer(t, "felhom-backup", []hub.StorageTarget{ {Name: "adat", Type: hub.StorageTypeLocalDir, MountPath: "/mnt/adat", BackingDevice: "", State: hub.StorageStateDisconnected}, })) for _, d := range disks { if bt, _ := d["backup_target"].(bool); bt { t.Errorf("non-target drive %v reports backup_target=true", d["name"]) } if gp, _ := d["guest_path"].(string); gp != "" { t.Errorf("an absent NON-target drive acquired guest path %q via the R-116 fallback — the "+ "BackupTarget gate failed", gp) } } }