From 5540fd4f854f5d4fdd64a8f3a77475a75de74d80 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 15:57:30 +0200 Subject: [PATCH] Find the catalogue's repository by its URL or seat source, and fail a plan whose catalogue repository is nowhere, so a take-in from a key no longer waits for ever (issue 1902) --- cmd/mesh-controller/key_takein.go | 46 ++++++++++-- .../key_takein_integration_test.go | 20 +++++ cmd/mesh-controller/predefined_head_test.go | 75 +++++++++++++++++++ 3 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 cmd/mesh-controller/predefined_head_test.go diff --git a/cmd/mesh-controller/key_takein.go b/cmd/mesh-controller/key_takein.go index 2dd76ece..ce5d2ff3 100644 --- a/cmd/mesh-controller/key_takein.go +++ b/cmd/mesh-controller/key_takein.go @@ -92,16 +92,42 @@ var predefinedAtTrunk = func(ctx context.Context, inv *inventory.Inventory) (pre if err != nil { return predefinedHead{}, err } - repository := "" + return readPredefinedHead(ctx, entries, formsOfSources(ctx, inv)) +} + +// formsOfSources reads how the sources are spelled on this mesh; a variable so a test names the forge's address. +var formsOfSources = newSourceForms + +// errNoCatalogueRepository says no module of the mesh is built from the catalogue's repository on the forge: not a wait, +// since no read of the forge makes one appear (issue 1902); the plan fails with these words. +var errNoCatalogueRepository = errors.New("no module of the catalogue names the repository " + + catalogue.CatalogueRepository + " on the forge") + +// catalogueRepositoryAmong is the catalogue's repository on the forge, as owner/name, found as catch-up finds the trunks +// of the repositories the catalogue builds from: a source in the URL form or the seat form, `.git` dropped (issue 1902). +func catalogueRepositoryAmong(entries []inventory.Entry, forms *sourceForms) (string, error) { for _, e := range entries { - if e.Source.Seat == gitSeat && path.Base(strings.Trim(e.Source.Repository, "/")) == catalogue.CatalogueRepository { - repository = strings.Trim(e.Source.Repository, "/") - break + if e.Source.Repository == "" { + continue + } + owner, name, onForge := forms.onTheForge(e.Source.Repository, e.Source.Seat) + if onForge && strings.EqualFold(name, catalogue.CatalogueRepository) { + return owner + "/" + name, nil } } - if repository == "" { - return predefinedHead{}, fmt.Errorf("no module of the catalogue names the repository %s on the forge", - catalogue.CatalogueRepository) + return "", errNoCatalogueRepository +} + +// readPredefinedHead is the list at the trunk head of the catalogue's repository, found among the modules' sources and +// read from the forge. +func readPredefinedHead(ctx context.Context, entries []inventory.Entry, forms *sourceForms) (predefinedHead, error) { + // A URL source is on the forge only once the forge's address is known; an address that cannot be read is a wait. + if _, err := forms.forgeAddress(ctx, forgeAddressWait); err != nil { + return predefinedHead{}, fmt.Errorf("the forge's address cannot be read: %w", err) + } + repository, err := catalogueRepositoryAmong(entries, forms) + if err != nil { + return predefinedHead{}, err } var heads []struct { SHA string `json:"sha"` @@ -143,7 +169,7 @@ var predefinedAtTrunk = func(ctx context.Context, inv *inventory.Inventory) (pre } // askGitea asks the forge's module one tool on the bus and reads its result into into. -func askGitea(ctx context.Context, tool string, args map[string]any, into any) error { +var askGitea = func(ctx context.Context, tool string, args map[string]any, into any) error { js, err := aBus() if err != nil { return err @@ -183,6 +209,10 @@ type keyTake struct { func takeInKey(ctx context.Context, inv *inventory.Inventory, keys keyReads, t keyTake) ( catalogue.Manifest, inventory.Build, error) { head, err := predefinedAtTrunk(ctx, inv) + if errors.Is(err, errNoCatalogueRepository) { + // Fails the plan, loudly, with these words (issue 1902): waiting would never end. + return catalogue.Manifest{}, inventory.Build{}, fmt.Errorf("%s is not judged: %w", t.Module, err) + } if err != nil { return catalogue.Manifest{}, inventory.Build{}, fmt.Errorf("%w: the list of predefined keys at the catalogue's "+ "trunk head cannot be read, so %s is not judged: %v", errKeyWaits, t.Module, err) diff --git a/cmd/mesh-controller/key_takein_integration_test.go b/cmd/mesh-controller/key_takein_integration_test.go index 0c5d0e70..dd8ee319 100644 --- a/cmd/mesh-controller/key_takein_integration_test.go +++ b/cmd/mesh-controller/key_takein_integration_test.go @@ -249,3 +249,23 @@ func TestTheSwitchsListReadsTheHeldBuildsAndTheKeys(t *testing.T) { t.Fatalf("A13: the switch's list is %+v", lines) } } + +// Issue 1902: the take-in with the lookup of the catalogue's repository NOT stubbed - the catalogue's modules are registered +// with a URL source and no seat, as every live row is - reads the list from the forge's wire alone and registers the module. +func TestATakeInFindsTheCataloguesRepositoryInTheURLForm(t *testing.T) { + open := theCatalogue(t) + ctx := t.Context() + was, wasForms := formsOfSources, askGitea + formsOfSources = func(context.Context, *inventory.Inventory) *sourceForms { return aFormsOnTheForge() } + t.Cleanup(func() { formsOfSources, askGitea = was, wasForms }) + aForgeAnsweringTheCatalogue(t) + if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "catalogue-anchor", Version: "1"}, + inventory.Source{Repository: "http://anchor.internal:20000/novox/mesh-catalog.git", Path: "modules/catalogue-anchor", + Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil { + t.Fatal(err) + } + keys, trunk := aKeyFor("sudo", "6", fpOf('7'), "novox/mesh-catalog", "modules/sudo", "c0ffee0123456789") + if _, _, err := takeInKey(ctx, open.inventory, keys, keyTake{Module: "sudo", Trunk: trunk, Ask: registrationAsk{Merge: trunk.Commit}}); err != nil { + t.Fatalf("a key was not taken in with the real lookup of the catalogue's repository: %v", err) + } +} diff --git a/cmd/mesh-controller/predefined_head_test.go b/cmd/mesh-controller/predefined_head_test.go new file mode 100644 index 00000000..dc31b5fc --- /dev/null +++ b/cmd/mesh-controller/predefined_head_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "github.com/novox/mesh-controller/internal/inventory" +) + +// aForgeAnsweringTheCatalogue stands in for the forge's wire alone (askGitea): the head of the catalogue's trunk and its +// predefined.json. It says which repository path was asked. +func aForgeAnsweringTheCatalogue(t *testing.T) *[]string { + t.Helper() + var asked []string + was := askGitea + askGitea = func(_ context.Context, _ string, args map[string]any, into any) error { + p, _ := args["path"].(string) + asked = append(asked, p) + switch { + case strings.Contains(p, "/commits"): + return json.Unmarshal([]byte(`[{"sha":"abcdef0123456789","commit":{"committer":{"date":"2026-10-23T12:00:00Z"}}}]`), into) + case strings.Contains(p, "/contents/"): + body, _ := json.Marshal(map[string]string{"encoding": "base64", "content": base64.StdEncoding.EncodeToString([]byte(`{}`))}) + return json.Unmarshal(body, into) + } + return errors.New("asked " + p) + } + t.Cleanup(func() { askGitea = was }) + return &asked +} + +func aFormsOnTheForge() *sourceForms { + return sourceFormsRead(context.Background(), time.Now, func(context.Context) (catalogue.World, error) { + return forgeHolding(float64(20000)), nil + }) +} + +// Issue 1902: the catalogue's repository is found among the modules' sources in the URL form (what every live row has) and +// in the seat form, `.git` dropped; the real lookup runs, only the forge's wire is a stand-in. +func TestTheCataloguesRepositoryIsFoundInTheURLFormAndTheSeatForm(t *testing.T) { + for name, source := range map[string]inventory.Source{ + "the URL form": {Repository: "http://anchor.internal:20000/novox/mesh-catalog.git", Path: "modules/sudo"}, + "the seat form": {Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/sudo"}, + } { + asked := aForgeAnsweringTheCatalogue(t) + entries := []inventory.Entry{ + {Manifest: catalogue.Manifest{Module: "other"}, Source: inventory.Source{Repository: "http://anchor.internal:20000/novox/mesh-delivery.git"}}, + {Manifest: catalogue.Manifest{Module: "sudo"}, Source: source}, + } + head, err := readPredefinedHead(t.Context(), entries, aFormsOnTheForge()) + if err != nil || head.Repository != "novox/mesh-catalog" || head.Commit != "abcdef0123456789" { + t.Fatalf("%s: the head is %+v (%v)", name, head, err) + } + if len(*asked) != 2 || !strings.HasPrefix((*asked)[0], "/repos/novox/mesh-catalog/commits") { + t.Errorf("%s: the forge was asked %v", name, *asked) + } + } +} + +// And a mesh where nothing is built from the catalogue's repository says so by name, as an error that is not a wait. +func TestNoModuleBuiltFromTheCataloguesRepositoryIsNamedNotWaitedFor(t *testing.T) { + aForgeAnsweringTheCatalogue(t) + entries := []inventory.Entry{{Manifest: catalogue.Manifest{Module: "other"}, + Source: inventory.Source{Repository: "http://anchor.internal:20000/novox/mesh-delivery.git"}}} + _, err := readPredefinedHead(t.Context(), entries, aFormsOnTheForge()) + if !errors.Is(err, errNoCatalogueRepository) || errors.Is(err, errKeyWaits) { + t.Fatalf("a mesh with no catalogue module: %v", err) + } +}