package storage import ( "path/filepath" "testing" ) func TestIntentStore_StatesAndPersistence(t *testing.T) { path := filepath.Join(t.TempDir(), "intent.json") s, err := OpenIntentStore(path) if err != nil { t.Fatalf("open: %v", err) } // new: absent → IntentNew if s.Get("uuid:A") != IntentNew { t.Fatalf("absent should be new, got %q", s.Get("uuid:A")) } if err := s.SetEnrolled("uuid:A"); err != nil { t.Fatal(err) } if err := s.SetEjected("uuid:B"); err != nil { t.Fatal(err) } if err := s.SetDecommissioned("uuid:C"); err != nil { t.Fatal(err) } // reload → states persisted s2, err := OpenIntentStore(path) if err != nil { t.Fatalf("reopen: %v", err) } if s2.Get("uuid:A") != IntentEnrolled || s2.Get("uuid:B") != IntentEjected || s2.Get("uuid:C") != IntentDecommissioned { t.Fatalf("states not persisted: A=%q B=%q C=%q", s2.Get("uuid:A"), s2.Get("uuid:B"), s2.Get("uuid:C")) } } func TestIntentStore_OnAbsent_ReplyRule(t *testing.T) { s, _ := OpenIntentStore(filepath.Join(t.TempDir(), "i.json")) _ = s.SetEjected("uuid:E") _ = s.SetDecommissioned("uuid:D") _ = s.SetEnrolled("uuid:N") // ejected → absent → enrolled (so a replug auto-mounts) if err := s.OnAbsent("uuid:E"); err != nil { t.Fatal(err) } if s.Get("uuid:E") != IntentEnrolled { t.Fatalf("ejected→absent should clear to enrolled, got %q", s.Get("uuid:E")) } // decommissioned survives absent _ = s.OnAbsent("uuid:D") if s.Get("uuid:D") != IntentDecommissioned { t.Fatalf("decommissioned must survive absent, got %q", s.Get("uuid:D")) } // enrolled unchanged by absent _ = s.OnAbsent("uuid:N") if s.Get("uuid:N") != IntentEnrolled { t.Fatalf("enrolled should stay enrolled, got %q", s.Get("uuid:N")) } } func TestIntentStore_EmptyDurableIDRefused(t *testing.T) { s, _ := OpenIntentStore(filepath.Join(t.TempDir(), "i.json")) if err := s.SetEnrolled(""); err == nil { t.Fatal("recording intent for an empty durable-id must be refused") } if s.Get("") != IntentNew { t.Fatal("empty durable-id must read as new") } } // ReconcilePermitted is the gate the reconciler uses: only an `enrolled` drive (intent-none) is // auto-mounted; new/ejected/decommissioned are not. This documents the decision table. func TestIntentStore_ReconcileGate(t *testing.T) { s, _ := OpenIntentStore(filepath.Join(t.TempDir(), "i.json")) _ = s.SetEnrolled("uuid:enr") _ = s.SetEjected("uuid:ej") _ = s.SetDecommissioned("uuid:dec") cases := map[string]bool{ "uuid:enr": true, // enrolled → reconcile "uuid:ej": false, // ejected → leave alone "uuid:dec": false, // decommissioned → never "uuid:new": false, // new → never auto-mount } for id, want := range cases { got := s.Get(id) == IntentEnrolled if got != want { t.Errorf("reconcile-permitted(%s): got %v want %v (intent %q)", id, got, want, s.Get(id)) } } }