diff --git a/cmd/mesh-controller/replay239_test.go b/cmd/mesh-controller/replay239_test.go new file mode 100644 index 00000000..46282a41 --- /dev/null +++ b/cmd/mesh-controller/replay239_test.go @@ -0,0 +1,80 @@ +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") + } +} diff --git a/testdata/store-tests b/testdata/store-tests index 936e5928..20fe4b16 100644 --- a/testdata/store-tests +++ b/testdata/store-tests @@ -43,3 +43,4 @@ ./cmd/mesh-controller TestOnlyATrustedFilesNamedKeyIsRefusedThroughAVerb ./cmd/mesh-controller TestReplay349 ./cmd/mesh-controller TestReplay348AFaultFromBeforeTheSendFailsNoGate +./cmd/mesh-controller TestReplay239AModuleNameIsNotTakenOverByAnotherRepository