From 0e413e3e7a84cfd8182b0f9e420c293323b1cb1c Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 21:40:08 +0200 Subject: [PATCH] Hold the forge's port to the same rule as every other provider's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The package registry was the one foundation port not resolved from what its module serves. Nothing in the controller had to change for it — `ports` on the forge moves its container, what it serves and what consumers are told, and the builder's carried binding is settable like any other mergeable file — but nothing said so, which is how it came to be special in the first place. Two tests over the catalogue's own manifests: the forge's port is given on a node and reaches what it serves, and the builder's carried binding takes the port from the node while keeping who the binding is with. novox/hq 04-ISSUES/085 --- .../catalogue/foundation_manifests_test.go | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/internal/catalogue/foundation_manifests_test.go b/internal/catalogue/foundation_manifests_test.go index 44f95d8..136b042 100644 --- a/internal/catalogue/foundation_manifests_test.go +++ b/internal/catalogue/foundation_manifests_test.go @@ -1,6 +1,7 @@ package catalogue import ( + "encoding/json" "fmt" "os" "reflect" @@ -82,3 +83,98 @@ func TestTheFilterModuleNeverFlushesTheRuleset(t *testing.T) { load["restart-on"]) } } + +// The package registry's port is the node's, like every other foundation port (novox/hq +// 04-ISSUES/085, ADR 0100). Two halves, because the forge is reached two ways: through what the +// module that serves it says it serves, and — for the genesis window, before any module provides +// `package-registry` at all — through the one binding the builder carries instead of resolving. + +func TestTheForgesPortIsGivenLikeAnyOtherProvidersPort(t *testing.T) { + forge := catalogueManifest(t, "gitea") + + // The catalogue's number is a default and the node's setting moves it. + given, err := GivenPorts(forge, []Layer{{From: "anchor", + Values: map[string]any{PortsSetting: map[string]any{"3000": float64(3100)}}}}) + if err != nil { + t.Fatalf("the forge's port cannot be given on a node: %v", err) + } + if given[3000] != 3100 { + t.Fatalf("the forge was given %v", given) + } + + // And every consumer of the package registry is told where the machine actually put it, + // because that is read from what the forge serves rather than written in the consumer. + if got := ServedOn(forge, "package-registry", given)["port"]; got != 3100 { + t.Errorf("the package registry is served on %v, not the port this node gave it", got) + } + if got := ServedOn(forge, "package-registry", nil)["port"]; got != float64(3000) { + t.Errorf("without a setting the forge serves %v, not the catalogue's port", got) + } +} + +// bindingIn is the package binding the builder carries, as the machine would receive it. +func bindingIn(t *testing.T, m Manifest, layers []Layer) map[string]any { + t.Helper() + for _, r := range m.Resources { + if fmt.Sprint(r["id"]) != "package-binding" { + continue + } + settled, err := ApplySettings(r, layers) + if err != nil { + t.Fatalf("the builder's package binding refused %v: %v", layers, err) + } + if settled["merge"] != nil || settled["protected"] != nil { + t.Fatal("the host would be sent fields it does not know") + } + var out map[string]any + if err := json.Unmarshal([]byte(fmt.Sprint(settled["content"])), &out); err != nil { + t.Fatalf("the builder's package binding is not a binding: %v", err) + } + return out + } + t.Fatal("the builder carries no package binding") + return nil +} + +func TestTheBuildersCarriedPackageBindingTakesThePortFromTheNode(t *testing.T) { + builder := catalogueManifest(t, "builder") + + // Nothing set: the catalogue's own number, which is what a mesh raised on the defaults uses. + serves := bindingIn(t, builder, nil)["serves"].(map[string]any) + if serves["port"] != float64(3000) { + t.Fatalf("the builder's binding defaults to %v", serves["port"]) + } + + // Given a port, the binding dials it — and the rest of what the forge serves survives, because + // a setting is merged into the module's own values rather than replacing them. + moved := bindingIn(t, builder, []Layer{{From: "anchor", + Values: map[string]any{"serves": map[string]any{"port": float64(3100)}}}}) + got := moved["serves"].(map[string]any) + if got["port"] != float64(3100) { + t.Errorf("the builder dials %v, not the port this node gave the package registry", got["port"]) + } + if got["scheme"] != "http" || got["npm-path"] != "/api/packages/novox/npm/" { + t.Errorf("setting the port lost the rest of what the forge serves: %v", got) + } + if moved["as"] != "mesh-builder" || moved["from"] != "gitea" { + t.Errorf("setting the port changed who the binding is with: %v", moved) + } +} + +func TestTheBuildersPackageBindingKeepsItsIdentity(t *testing.T) { + builder := catalogueManifest(t, "builder") + for _, key := range []string{"provision", "from", "as"} { + var refused error + for _, r := range builder.Resources { + if fmt.Sprint(r["id"]) != "package-binding" { + continue + } + _, refused = ApplySettings(r, []Layer{{From: "anchor", + Values: map[string]any{key: "something else"}}}) + } + if refused == nil { + t.Errorf("%q can be set on the builder's package binding, which is not a port but who "+ + "the binding is with", key) + } + } +}