From 0e413e3e7a84cfd8182b0f9e420c293323b1cb1c Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 21:40:08 +0200 Subject: [PATCH 1/2] 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) + } + } +} -- 2.54.0 From 729537e74516d776ae6634db8bf305b2ecbb2ecf Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 21:56:51 +0200 Subject: [PATCH 2/2] Tie the builder's carried binding to what the forge serves, and hold `at` shut The builder carries a binding because at genesis nothing provides `package-registry` to resolve one from; once the forge is a module the same consumer is told what the forge serves. Nothing held the two to the same number, so the catalogue could drift into dialling one port before the forge is assigned and another after. And `at` is now protected, for the reason it had to be: a setting that moves it points the builder, and the registry password it sends, at a host somebody else chose. novox/hq 04-ISSUES/085 --- .../catalogue/foundation_manifests_test.go | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/catalogue/foundation_manifests_test.go b/internal/catalogue/foundation_manifests_test.go index 136b042..c07d5f4 100644 --- a/internal/catalogue/foundation_manifests_test.go +++ b/internal/catalogue/foundation_manifests_test.go @@ -161,9 +161,27 @@ func TestTheBuildersCarriedPackageBindingTakesThePortFromTheNode(t *testing.T) { } } +// The two halves are one number. The builder carries a binding because at genesis nothing provides +// `package-registry` to resolve one from; the day the forge is a module, the same consumer is told +// what the forge serves. They have to start from the same port, or a mesh raised on the defaults +// dials one number before the forge is assigned and another after. +func TestTheBuildersCarriedBindingStartsWhereTheForgeServes(t *testing.T) { + forge := ServedOn(catalogueManifest(t, "gitea"), "package-registry", nil) + carried := bindingIn(t, catalogueManifest(t, "builder"), nil)["serves"].(map[string]any) + for _, key := range []string{"port", "scheme", "npm-path"} { + if fmt.Sprint(forge[key]) != fmt.Sprint(carried[key]) { + t.Errorf("the forge serves %s %v and the builder's carried binding says %v — the two "+ + "halves of the same registry have drifted apart in the catalogue", + key, forge[key], carried[key]) + } + } +} + func TestTheBuildersPackageBindingKeepsItsIdentity(t *testing.T) { builder := catalogueManifest(t, "builder") - for _, key := range []string{"provision", "from", "as"} { + // `at` above all: a setting that moves it points the builder, and the registry password it + // sends as basic auth, at a host somebody else chose. + for _, key := range []string{"provision", "from", "at", "as"} { var refused error for _, r := range builder.Resources { if fmt.Sprint(r["id"]) != "package-binding" { -- 2.54.0