55c896624f
Drives are visible in-guest only at the STABLE /mnt/felhom-drives/<name>; the registered path + HDD_PATH + FileBrowser source repoint there while agent calls map back to raw /mnt/<name> (agentWhere). Enroll binds-under-parent before register. Drive-absent GATE (planDriveGates + 30s driveGateLoop) stops/blocks apps when a drive vanishes and auto-restarts on return; start-gate refuses start when the drive is absent. H1 endpoints (disconnect/reconnect/restart-apps) routed onto host-side ops. Non-hollow tests + companions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.8 KiB
Go
70 lines
2.8 KiB
Go
package web
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
func TestAgentWhere(t *testing.T) {
|
|
cases := map[string]string{
|
|
"/mnt/felhom-drives/felhom-usb": "/mnt/felhom-usb", // stable → raw
|
|
"/mnt/felhom-usb": "/mnt/felhom-usb", // legacy raw → raw (idempotent)
|
|
"/mnt/felhom-drives/x": "/mnt/x",
|
|
}
|
|
for in, want := range cases {
|
|
if got := agentWhere(in); got != want {
|
|
t.Errorf("agentWhere(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStablePathForName(t *testing.T) {
|
|
if got := stablePathForName("felhom-usb"); got != "/mnt/felhom-drives/felhom-usb" {
|
|
t.Errorf("stablePathForName = %q", got)
|
|
}
|
|
}
|
|
|
|
// TestPlanDriveGates pins the gate's pure decision across the four meaningful states.
|
|
//
|
|
// COMPANION GUARD: a trivial impl that gates every absent path regardless of the disconnected flag would
|
|
// re-stop an already-disconnected drive (and never return it); one that ignores presence would never
|
|
// gate. Both fail here.
|
|
func TestPlanDriveGates(t *testing.T) {
|
|
paths := []settings.StoragePath{
|
|
{Path: "/mnt/felhom-drives/usb"}, // present + connected → no action
|
|
{Path: "/mnt/felhom-drives/flash"}, // ABSENT + connected → STOP
|
|
{Path: "/mnt/felhom-drives/back", Disconnected: true}, // present + disconnected → RETURN
|
|
{Path: "/mnt/felhom-drives/gone", Disconnected: true}, // ABSENT + disconnected → no action (steady)
|
|
{Path: "/mnt/felhom-drives/dead", Decommissioned: true}, // decommissioned → never touched
|
|
}
|
|
disks := []agentapi.DiskInfo{
|
|
{MountPath: "/mnt/usb", GuestPath: "/mnt/felhom-drives/usb", State: "attached"},
|
|
{MountPath: "/mnt/back", GuestPath: "/mnt/felhom-drives/back", State: "attached"},
|
|
// flash + gone + dead report NO present disk
|
|
}
|
|
actions := map[string]gateAction{}
|
|
for _, a := range planDriveGates(paths, disks) {
|
|
actions[a.Path] = a
|
|
}
|
|
if len(actions) != 2 {
|
|
t.Fatalf("expected exactly 2 actions (stop flash, return back), got %d: %+v", len(actions), actions)
|
|
}
|
|
if a, ok := actions["/mnt/felhom-drives/flash"]; !ok || !a.Stop || a.Return {
|
|
t.Errorf("flash should STOP (absent+connected): %+v", a)
|
|
}
|
|
if a, ok := actions["/mnt/felhom-drives/back"]; !ok || !a.Return || a.Stop || a.Raw != "/mnt/back" {
|
|
t.Errorf("back should RETURN with raw /mnt/back (present+disconnected): %+v", a)
|
|
}
|
|
if _, gated := actions["/mnt/felhom-drives/usb"]; gated {
|
|
t.Errorf("usb (present+connected) must not be gated")
|
|
}
|
|
if _, acted := actions["/mnt/felhom-drives/gone"]; acted {
|
|
t.Errorf("gone (absent+already-disconnected) is steady — no action")
|
|
}
|
|
if _, acted := actions["/mnt/felhom-drives/dead"]; acted {
|
|
t.Errorf("decommissioned drive must never be gated")
|
|
}
|
|
}
|