Files
felhom-controller/controller/internal/web/decommission_test.go
T
admin f2596ea433 B2b: decommission orchestration + missing-storage indicator + re-enroll fix (v0.65.0)
agentapi.Decommission + handleStorageDecommission (migrate-all-or-none, Change 2):
migrate-then-decommission via the migration done-hook, or decommission-anyway (stop
apps, keep HDD_PATH). 'Hiányzó tárhely' badge on dashboard/stacks/app card when an
app's drive is decommissioned/disconnected/absent. Change 4: registerStoragePath
clears the decommissioned marker on re-enroll (ClearDecommissioned had no callers).
Non-hollow tests incl. mutation-proven Change-4 companion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:08:52 +02:00

111 lines
4.5 KiB
Go

package web
import (
"context"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// TestFinalizeDecommission_SoftMarksAndCallsAgent: finalizeDecommissionWith soft-marks the registry
// (entry retained, MigratedTo set) and calls the agent's Decommission. Used by both the migrate-done
// hook (MigratedTo=target) and decommission-anyway (MigratedTo="").
func TestFinalizeDecommission_SoftMarksAndCallsAgent(t *testing.T) {
s := testServer(t)
_ = s.settings.AddStoragePath(settings.StoragePath{Path: "/mnt/old", Schedulable: true, IsDefault: true})
agent := &mockAgent{}
if err := s.finalizeDecommissionWith(context.Background(), agent, "/mnt/old", "/mnt/new"); err != nil {
t.Fatalf("finalize: %v", err)
}
// soft-marked, entry retained
if !s.settings.IsDecommissioned("/mnt/old") {
t.Errorf("path not soft-marked decommissioned")
}
found := false
for _, sp := range s.settings.GetStoragePaths() {
if sp.Path == "/mnt/old" {
found = true
if sp.MigratedTo != "/mnt/new" {
t.Errorf("MigratedTo = %q, want /mnt/new", sp.MigratedTo)
}
if sp.Schedulable || sp.IsDefault {
t.Errorf("decommissioned entry should not be schedulable/default")
}
}
}
if !found {
t.Errorf("entry was REMOVED — soft marker must retain it (blocks A1 resurrection)")
}
// agent told to decommission
if len(agent.decommissionCalls) != 1 || agent.decommissionCalls[0] != "/mnt/old" {
t.Errorf("agent.Decommission calls = %v, want [/mnt/old]", agent.decommissionCalls)
}
}
// TestOnMigrationDone_UnflaggedIsNoop: a plain migrate-all completion (DecommissionOnDone=false) must
// NOT decommission the source. (The flagged path's effects are covered by the finalize test; it would
// require a live agent client here.) Companion: if the early-return guard were removed, this would try
// to reach agentClient() and the path would change — so the no-op assertion pins the guard.
func TestOnMigrationDone_UnflaggedIsNoop(t *testing.T) {
s := testServer(t)
_ = s.settings.AddStoragePath(settings.StoragePath{Path: "/mnt/old", Schedulable: true})
s.OnMigrationDone(&stacks.MigrationJob{Source: "/mnt/old", Target: "/mnt/new", DecommissionOnDone: false})
if s.settings.IsDecommissioned("/mnt/old") {
t.Errorf("unflagged migration must not decommission the source")
}
}
// TestReEnrollClearMarker_Change4: re-enrolling a decommissioned path clears the marker + restores
// Schedulable so its apps' missing-storage indicator clears. (Companion: a non-decommissioned path is
// untouched — returns false.)
func TestReEnrollClearMarker_Change4(t *testing.T) {
s := testServer(t)
_ = s.settings.AddStoragePath(settings.StoragePath{Path: "/mnt/usb", Schedulable: true})
_ = s.settings.SetDecommissioned("/mnt/usb", "")
if !s.settings.IsDecommissioned("/mnt/usb") {
t.Fatal("precondition: should be decommissioned")
}
cleared, err := s.reEnrollClearMarker("/mnt/usb")
if err != nil {
t.Fatalf("reEnrollClearMarker: %v", err)
}
if !cleared {
t.Errorf("expected cleared=true for a decommissioned path")
}
if s.settings.IsDecommissioned("/mnt/usb") {
t.Errorf("marker not cleared")
}
if !s.settings.IsStoragePathSchedulable("/mnt/usb") {
t.Errorf("Schedulable not restored")
}
// companion: a non-decommissioned path is a no-op
if c, _ := s.reEnrollClearMarker("/mnt/usb"); c {
t.Errorf("non-decommissioned path should return cleared=false")
}
}
// TestMissingStorageLabel: an app's HDD_PATH on a decommissioned/disconnected/absent registry path is
// "missing"; on a present+schedulable path it is not; an empty HDD_PATH (SSD) is never missing.
func TestMissingStorageLabel(t *testing.T) {
s := testServer(t)
_ = s.settings.AddStoragePath(settings.StoragePath{Path: "/mnt/live", Label: "Élő", Schedulable: true})
_ = s.settings.AddStoragePath(settings.StoragePath{Path: "/mnt/dead", Label: "Holt", Schedulable: true})
_ = s.settings.SetDecommissioned("/mnt/dead", "")
if _, missing := s.missingStorageLabel(""); missing {
t.Errorf("empty HDD_PATH (SSD) must not be missing")
}
if _, missing := s.missingStorageLabel("/mnt/live"); missing {
t.Errorf("present+schedulable path must not be missing")
}
if label, missing := s.missingStorageLabel("/mnt/dead"); !missing || label != "Holt" {
t.Errorf("decommissioned path: missing=%v label=%q, want true/Holt", missing, label)
}
if label, missing := s.missingStorageLabel("/mnt/gone"); !missing || label == "" {
t.Errorf("unregistered path: missing=%v label=%q, want true/non-empty", missing, label)
}
}