//go:build integration package main import ( "encoding/json" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // Replay of novox/hq issue 239 (2026-10-04): the photo app's module `photos` was built from the app's own // repository, and the catalogue held a stub of the same name in `modules/photos` that had never run. Rebuilding // `photos` "to main" built the catalogue's main instead; the controller registered the outcome and recorded the // catalogue as the module's new source, and the next push replaced the app with the stub: two sites' containers // and five of its six routes went. Nothing compared the build's source with the source recorded before it. // // Written only with what the controller had before its fix (mesh-controller #174, hq ADR 0266 §7): a mesh, the // inventory's RegisterModule and SourceOf, a kept build request, and takeIn for the build's outcome — so the // prover (mesh-lab replays/cmd/prove) lays it over the commit before. Its outcome is the issue's acceptance: a // build asked through a verb, of a module's name from a repository other than the one the module is registered // from, is refused, says both sources, and leaves the module and its recorded source as they were. Needs a // store: MESH_TEST_POSTGRES. func TestReplay239AModuleNameIsNotTakenOverByAnotherRepository(t *testing.T) { open := aMesh(t) ctx := t.Context() inv := open.inventory // Every verb's command runs marked as the serving controller's: never the terminal. t.Setenv("MESH_SERVED_BY_THE_CONTROLLER", "1") register := func(raw, repository, path string) { t.Helper() m, err := catalogue.ParseManifest([]byte(raw)) if err != nil { t.Fatal(err) } if err := inv.RegisterModule(ctx, m, inventory.Source{Repository: repository, Seat: "git", Path: path, Ref: "main", BuiltFrom: "1111111111111111"}); err != nil { t.Fatal(err) } } // The app, built from its own repository; and another module of the catalogue, so the catalogue is a // repository the mesh builds from. register(`{"module": "photos", "version": "app"}`, "novox/photos", "") register(`{"module": "sudo", "version": "1"}`, "novox/mesh-catalog", "modules/sudo") // The rebuild "to main", asked through the build verb: the catalogue's stub of the same name. const id = "build-photos-from-the-catalogue" if err := inv.RecordBuildRequest(ctx, inventory.BuildRequest{ID: id, Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/photos", Ref: "main", For: "build"}); err != nil { t.Fatal(err) } stub, _ := json.Marshal(map[string]any{"module": "photos", "version": "stub"}) _, _, err := takeIn(ctx, inv, link.BuildResult{ID: id, Repository: "http://forge.internal:20000/novox/mesh-catalog.git", Path: "modules/photos", Ref: "main", On: "anchor", Commit: "2222222222222222", Manifest: stub, Trunk: "main", OnTrunk: true, Branches: []string{"main"}, Source: &link.SourceOnSeat{Seat: "git", Repository: "novox/mesh-catalog"}}) if err == nil { t.Error("a build of photos from the catalogue was registered over the app's own repository") } else if !strings.Contains(err.Error(), "novox/photos") || !strings.Contains(err.Error(), "novox/mesh-catalog") { t.Errorf("the refusal does not say both sources: %v", err) } if src, err := inv.SourceOf(ctx, "photos"); err != nil { t.Fatal(err) } else if src.Repository != "novox/photos" { t.Errorf("photos is now recorded as built from %q, not its own repository", src.Repository) } shelf, err := inv.Catalogue(ctx) if err != nil { t.Fatal(err) } if got := shelf["photos"].Version; got != "app" { t.Errorf("photos is now %q: the next push sends the catalogue's stub", got) } if _, found, _ := inv.BuildByID(ctx, id); !found { t.Error("the refused build is not recorded") } }