package main import ( "context" "errors" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/conditions" ) // **No declaration is sent that names an image the artifact store does not hold** (novox/hq issue 1742). const storeAt = "novox.internal:5100" // automx is the image the send of 2026-10-21 17:13 named and the store lacked; the others are held. const ( automxImage = storeAt + "/mailu/automx@sha256:c62150b8e2cbcc18cb911b098c2877f928761a1e6251667f39ea18251ea35591" imapImage = storeAt + "/mailu/imap@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ) func aMailSend() sendable { return sendable{ Resources: []map[string]any{ {"id": "mailu.automx", "type": "container", "image": automxImage}, {"id": "mailu.automx-config", "type": "file", "path": "/etc/automx.conf"}, {"id": "mailu.imap", "type": "container", "image": imapImage}, {"id": "other.web", "type": "container", "image": "docker.io/library/nginx@sha256:" + strings.Repeat("b", 64)}, {"id": "dns.dnsmasq", "type": "container", "image": storeAt + "/dnsmasq/main@sha256:" + strings.Repeat("c", 64)}, }, moduleOf: map[string]string{"mailu.automx": "automx", "mailu.automx-config": "automx", "mailu.imap": "mailu", "other.web": "other", "dns.dnsmasq": "dnsmasq"}, Builds: map[string]string{"automx": "b2f5ebb2new", "mailu": "m2", "other": "o1", "dnsmasq": "d1"}, } } func storeHolding(missing ...string) (storeHolds, *[]string) { var asked []string return func(_ context.Context, reference string) (bool, error) { asked = append(asked, reference) for _, m := range missing { if strings.Contains(reference, m) { return false, nil } } return true, nil }, &asked } // 1742/AC1: given a send naming automx's image, which the store does not hold, when the declaration is judged, then // every resource of automx is out of it, automx is among the modules left out with the reason, its build is the one the // machine was last sent, and every other module — an image of another registry among them — is as composed. func TestAModuleWhoseImageTheStoreLacksIsLeftOutOfTheSend(t *testing.T) { s := aMailSend() holds, asked := storeHolding("mailu/automx@") found := holdBackMissingImages(t.Context(), "novox", &s, storeAt, holds, map[string]string{"automx": "b2f5ebb2old"}) if len(found) != 1 || found[0].Module != "automx" || found[0].Node != "novox" || found[0].Image != automxImage { t.Fatalf("found %+v", found) } var ids []string for _, r := range s.Resources { ids = append(ids, r["id"].(string)) } if got := strings.Join(ids, ","); got != "mailu.imap,other.web,dns.dnsmasq" { t.Errorf("the declaration keeps %s", got) } if len(s.LeftOut) != 1 || s.LeftOut[0] != "automx" || !strings.Contains(s.leftOutWhy["automx"], "not in the artifact store") { t.Errorf("left out %v because %v", s.LeftOut, s.leftOutWhy) } if s.Builds["automx"] != "b2f5ebb2old" || s.Builds["mailu"] != "m2" || s.Builds["other"] != "o1" { t.Errorf("builds %v", s.Builds) } for _, a := range *asked { if strings.Contains(a, "docker.io") { t.Errorf("the store was asked about an image of another registry: %s", a) } } } // 1742/AC2: given a module never sent to the machine, when its image is missing, then it has no build in the record of the // send; given the store does not answer, then nothing is held back for a question unanswered. func TestAnUnansweredStoreHoldsNothingBackAndANewModuleHasNoBuild(t *testing.T) { s := aMailSend() holds, _ := storeHolding("mailu/automx@") found := holdBackMissingImages(t.Context(), "novox", &s, storeAt, holds, nil) if len(found) != 1 { t.Fatalf("%+v", found) } if _, has := s.Builds["automx"]; has { t.Errorf("a module never sent is recorded as sent: %v", s.Builds) } s = aMailSend() silent := func(context.Context, string) (bool, error) { return false, errors.New("registry: connection refused") } if found := holdBackMissingImages(t.Context(), "novox", &s, storeAt, silent, nil); len(found) != 0 || len(s.LeftOut) != 0 || len(s.Resources) != 5 { t.Errorf("an unanswered store held back %+v, left out %v", found, s.LeftOut) } } // 1742/AC3: a module left out for its image raises a condition for the operator naming the module, the machine and the // image in the evidence, plain words, and no answer to give; a mesh whose images are all held raises nothing. func TestAMissingImageIsRaisedForTheOperator(t *testing.T) { f := calm(time.Date(2026, 10, 21, 17, 40, 0, 0, time.UTC)) if got := watchMissingImages(f); len(got) != 0 { t.Fatalf("%+v", got) } f.images = []imageFacts{{missingImage{Node: "novox", Module: "automx", Image: automxImage}}} got := watchMissingImages(f) if len(got) != 1 { t.Fatalf("%+v", got) } o := got[0] if o.Kind != "image-missing" || o.Key() != "module.automx.novox.image-missing" || o.Resolver != conditions.ResolverOperator || o.Severity != conditions.Warning || o.Machine != "novox" { t.Fatalf("%+v (%s)", o, o.Key()) } if !strings.Contains(o.Said, "c62150b8") || !strings.Contains(o.Headline+o.Explanation, "automx") { t.Errorf("words: %q / %q / %q", o.Headline, o.Explanation, o.Said) } w := conditions.Words{Headline: o.Headline, Explanation: o.Explanation, Resolved: o.Resolved, Needs: o.Needs} if why, ok := conditions.PlainWords(w, "novox"); !ok { t.Errorf("not plain: %s", why) } }