package apply import ( "context" "os" "path/filepath" "strings" "testing" "github.com/novox/mesh-host/internal/store" ) // A take is a comparison (novox/hq ADR 0163): while a module's container is held, the host reports // the found image and its age beside the declared one, the networks and who else is on them, the // mounts and the ports — and says when the declared image is the older. func TestAHeldContainerCarriesTheFactsATakeCompares(t *testing.T) { dir, page, m := predecessor(t) m.containers["hello-web"].image = "web:1.27" m.containers["hello-web"].imageID = "sha256:found" m.containers["hello-web"].networks = []string{"predecessor_default"} m.containers["hello-web"].mounts = []string{"/srv/web:/data"} m.containers["hello-web"].ports = []string{"80/tcp>0.0.0.0:8080"} m.images = map[string]string{"sha256:found": "2026-09-17T10:00:00Z", pinned: "2026-08-20T10:00:00Z"} m.members = map[string][]string{"predecessor_default": {"hello-web", "office", "db"}} _, state := applyAdopted(t, adopted(t, untakenWeb, webResources(page)), store.State{}, m, dir) h, ok := state.HeldAt("hello-web.server") if !ok || h.Facts == nil { t.Fatalf("a held container carries no facts: %+v", h) } f := h.Facts if f.Image != "web:1.27" || f.ImageCreated != "2026-09-17T10:00:00Z" { t.Errorf("the found image and its age: %+v", f) } if f.DeclaredImage != pinned || f.DeclaredImageCreated != "2026-08-20T10:00:00Z" || !f.Downgrade { t.Errorf("the declared image, its age, and that it is a downgrade: %+v", f) } if got := f.Networks["predecessor_default"]; len(got) != 2 || got[0] != "db" || got[1] != "office" { t.Errorf("the neighbours on the found network, without the container itself: %v", f.Networks) } if len(f.Mounts) != 1 || f.Mounts[0] != "/srv/web:/data" || len(f.Ports) != 1 || f.Ports[0] != "80/tcp>0.0.0.0:8080" { t.Errorf("mounts and ports as found: %+v", f) } // And the held file carries how the declared content differs from what was found. p, ok := state.HeldAt("hello-web.page") if !ok || p.Facts == nil || !p.Facts.Differs { t.Fatalf("a held file that differs from the declared content does not say so: %+v", p) } joined := strings.Join(p.Facts.Difference, "\n") if !strings.Contains(joined, "- the predecessor's page") || !strings.Contains(joined, "+ the mesh's page") { t.Errorf("the difference does not show what is lost and what is new: %q", joined) } _ = os.Remove(filepath.Join(dir, "unused")) } // A declared image not yet on the machine leaves its age unknown and the comparison undecided. func TestAnImageNotYetPulledLeavesTheDowngradeUndecided(t *testing.T) { dir, page, m := predecessor(t) m.containers["hello-web"].image = "web:1.27" m.containers["hello-web"].imageID = "sha256:found" m.images = map[string]string{"sha256:found": "2026-09-17T10:00:00Z"} _, state := applyAdopted(t, adopted(t, untakenWeb, webResources(page)), store.State{}, m, dir) h, _ := state.HeldAt("hello-web.server") if h.Facts == nil || h.Facts.DeclaredImageCreated != "" || h.Facts.Downgrade { t.Fatalf("an unknown declared age decided a downgrade: %+v", h.Facts) } } func TestTheDifferenceIsWhatIsLostAndWhatIsNew(t *testing.T) { differs, lines := differenceOf("a\nprivate scope: local\nb\n", "a\nb\nupstream: public\n") if !differs || len(lines) != 2 || lines[0] != "- private scope: local" || lines[1] != "+ upstream: public" { t.Fatalf("got %v %v", differs, lines) } if differs, lines := differenceOf("same\n", "same\n"); differs || lines != nil { t.Fatalf("identical content differs: %v %v", differs, lines) } } // What runs on the machine that the mesh neither wrote nor holds is reported (ADR 0163). func TestStraysAreWhatRunsHereThatNobodyAsked(t *testing.T) { m := &machine{containers: map[string]*fakeContainer{ "hello-web": {id: "ours", running: true, image: "web:1"}, "gitea-old": {id: "left-behind", running: true, image: "gitea:1.22"}, "held-thing": {id: "found", running: true, image: "x:1"}, }} known := store.State{ Resources: []store.Applied{{ID: "hello-web.server", Type: "container", Target: "hello-web"}}, Held: []store.Held{{ID: "other.server", Kind: "container", Target: "held-thing"}}, } strays, err := Strays(context.Background(), m.run, known) if err != nil { t.Fatal(err) } if len(strays) != 1 || strays[0].Name != "gitea-old" || !strings.Contains(strays[0].Detail, "gitea:1.22") { t.Fatalf("strays: %+v", strays) } }