package catalogue import ( "encoding/json" "strings" "testing" ) // novox/hq ADR 0377 ยง3: a move under a module's own `record` rolls out when it recreates none of its containers // and moves none of its data, judged from the two builds' manifests. func TestWhatAMoveUnderRecordHoldsFor(t *testing.T) { base := func() Manifest { raw := `{"module":"mailu","tools":["mailu_status"], "requires":["postgres-database"], "data":{"own":[{"id":"mail","path":"${dir:data-mail}","class":"valuable"}]}, "bundles":[{"name":"tools","source":"store/mailu-tools@sha256:aaa","digest":"sha256:aaa"}], "resources":[ {"id":"state","type":"directory"}, {"id":"mesh-state","type":"directory"}, {"id":"data-mail","type":"directory"}, {"id":"config-env","type":"file","path":"${dir:state}/mailu.env","content":"A=1"}, {"id":"runtime-config","type":"file","path":"${dir:mesh-state}/config.json","content":"{}"}, {"id":"inbox-note","type":"file","path":"${dir:data-mail}/README","content":"x"}, {"id":"imap","type":"container","image":"registry/imap@sha256:1","volumes":["${dir:data-mail}:/mail"], "env-file":["${dir:state}/mailu.env"]}, {"id":"watcher","type":"process","command":"watch"}]}` var m Manifest if err := json.Unmarshal([]byte(raw), &m); err != nil { t.Fatal(err) } return m } resource := func(m *Manifest, id string) map[string]any { for _, r := range m.Resources { if r["id"] == id { return r } } t.Fatalf("no resource %s", id) return nil } cases := []struct { name string change func(m *Manifest) holds string // empty: it rolls out }{ {"nothing", func(*Manifest) {}, ""}, {"a tools bundle", func(m *Manifest) { m.Bundles[0].Digest, m.Bundles[0].Source = "sha256:bbb", "store/mailu-tools@sha256:bbb" }, ""}, {"the tool list", func(m *Manifest) { m.Tools = append(m.Tools, "mailu_queue") }, ""}, {"a file no container mounts", func(m *Manifest) { resource(m, "runtime-config")["content"] = `{"a":1}` }, ""}, {"a container's image", func(m *Manifest) { resource(m, "imap")["image"] = "registry/imap@sha256:2" }, "recreates 1 of mailu's 1"}, {"a container's declaration", func(m *Manifest) { resource(m, "imap")["health"] = map[string]any{"kind": "runtime"} }, "recreates 1"}, {"a container added", func(m *Manifest) { m.Resources = append(m.Resources, map[string]any{"id": "smtp", "type": "container", "image": "x"}) }, "recreates"}, {"a mounted file", func(m *Manifest) { resource(m, "config-env")["content"] = "A=2" }, "which its container imap mounts"}, {"a file within declared data", func(m *Manifest) { resource(m, "inbox-note")["content"] = "y" }, "which its container imap mounts"}, {"a directory holding declared data", func(m *Manifest) { resource(m, "data-mail")["mode"] = "0700" }, "data-mail"}, {"the data declaration", func(m *Manifest) { m.Data.Own[0].Class = ClassIrreplaceable }, "data declaration"}, {"a provider", func(m *Manifest) { m.Requires = append(m.Requires, "s3") }, "what it requires"}, {"what it provides", func(m *Manifest) { m.Provides = append(m.Provides, Offer{Name: "smtp"}) }, "what it provides"}, {"a process of a module with data", func(m *Manifest) { resource(m, "watcher")["command"] = "watch2" }, "long-running process watcher"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { from, to := base(), base() c.change(&to) got := Holds(from, to) switch { case c.holds == "" && got != "": t.Fatalf("held: %s", got) case c.holds != "" && !strings.Contains(got, c.holds): t.Fatalf("holds %q; want it to say %q", got, c.holds) } }) } // A data directory nothing mounts still holds its data. from, to := base(), base() from.Data.Own = append(from.Data.Own, DataItem{ID: "archive", Path: "${dir:mesh-state}/archive", Class: "valuable"}) to.Data = &Data{Own: append([]DataItem(nil), from.Data.Own...)} resource(&to, "runtime-config")["path"] = "${dir:mesh-state}/archive/config.json" resource(&from, "runtime-config")["path"] = "${dir:mesh-state}/archive/config.json" resource(&to, "runtime-config")["content"] = "new" if got := Holds(from, to); !strings.Contains(got, "lies within its data archive") { t.Fatalf("a file within data nothing mounts: %q", got) } // A process of a module with no data of its own may change. from, to = base(), base() from.Data, to.Data = nil, nil resource(&to, "watcher")["command"] = "watch2" if got := Holds(from, to); got != "" { t.Fatalf("a process of a module with no data held: %s", got) } }