From 95556b8def3f3f21e3d037617d73f97606e6ea1f Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 13 Oct 2026 07:02:05 +0200 Subject: [PATCH] Add the replay of issue 239: a module's name is not taken over by a build from another repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix (#174, hq ADR 0266 §7) refuses an outcome built from a repository other than the module's registered one; nothing replayed the photo app's outage that showed why. The replay uses only what the controller had before the fix, so the prover can show it fails there and passes on the fix. --- cmd/mesh-controller/replay239_test.go | 80 +++++++++++++++++++++++++++ testdata/store-tests | 1 + 2 files changed, 81 insertions(+) create mode 100644 cmd/mesh-controller/replay239_test.go 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 -- 2.54.0