R4g+R5 (mesh-controller): the controller asks no build, build-agent and the merge gate go, register by reference (design 47) #448

Merged
jschoubben merged 38 commits from feat/r4g into main 2026-10-23 17:50:22 +00:00
3 changed files with 95 additions and 9 deletions
Showing only changes of commit 2db5dcdcb4 - Show all commits
+21 -9
View File
@@ -79,6 +79,20 @@ func registerCommand(ctx context.Context, args []string) error {
return err
}
defer closeKeys()
return registerNow(ctx, open, keys, hand, module, repository, path, references, given)
}
// registerKeys is what a registration reads of the indexer and the builder (builderkeys.Reader, or a test's).
type registerKeys interface {
keyReads
ModuleAt(ctx context.Context, module, commit string) (sdkdelivery.Module, error)
}
// registerNow is the registration once its arguments are read and the terminal is known: the module registered from its
// key, or at the references given. **The hand act is written after the registration, and only when it was made** (ADR
// 0526 clause 3): a take-in that waited or was refused did nothing by hand.
func registerNow(ctx context.Context, open *stores, keys registerKeys, hand handActFlags, module, repository, path string,
references map[string]string, given []string) error {
trunk, err := keys.ModuleAt(ctx, module, "")
if errors.Is(err, builderkeys.ErrNoValue) {
return fmt.Errorf("the indexer holds no trunk value of %s: a module is registered from the builder's key of a "+
@@ -99,15 +113,6 @@ func registerCommand(ctx context.Context, args []string) error {
}
take.Hand = references
}
// And written down before it is done, with its why (novox/hq to-be 45 §7): a placement by hand is not undone by the
// next push, and the log says who put it there.
if take.Hand != nil {
act := []string{module, repository, positionalPath(path)}
for _, g := range given {
act = append(act, "--reference", g)
}
hand.record(ctx, "register", act)
}
manifest, kept, err := takeInKey(ctx, open.inventory, keys, take)
if errors.Is(err, errKeyWaits) {
return fmt.Errorf("%s is not registered yet, and nothing was asked: %v. Run register again once its key is done",
@@ -117,6 +122,13 @@ func registerCommand(ctx context.Context, args []string) error {
return err
}
if take.Hand != nil {
// Written down with its why (novox/hq to-be 45 §7) now that it is done: a placement by hand is not undone by the
// next push, and the log says who put it there.
act := []string{module, repository, positionalPath(path)}
for _, g := range given {
act = append(act, "--reference", g)
}
hand.record(ctx, "register", act)
fmt.Printf("%s %s registered at the references given (%s at %.8s), as asked at the controller's terminal, "+
"recorded as a build by hand; assigned nowhere\n", manifest.Module, manifest.Version, repository, kept.Commit)
return nil
@@ -0,0 +1,65 @@
//go:build integration
package main
import (
"context"
"flag"
"testing"
"time"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-controller/internal/builderkeys"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// C8b (ADR 0526 clause 3; issue 1888): `register --reference` writes its hand act, with the why, only once the module is
// registered; a take-in that waits writes none.
func TestARegistrationAtReferencesWritesItsHandActOnlyWhenItWasMade(t *testing.T) {
open := theCatalogue(t)
ctx := t.Context()
b := aBuilderOnTheBus(t)
if err := b.js.EnsureControllerBuckets(); err != nil {
t.Fatal(err)
}
api, err := jetstream.New(b.js.Conn())
if err != nil {
t.Fatal(err)
}
keys := builderkeys.Reader{JS: api}
fp := fpOf('8')
b.indexed(t, "sudo", "7", "novox/mesh-catalog", "modules/sudo", "c0ffee0123456789", fp)
ref := "artifact-store://sudo/a/blobs/" + shaOf("hand")
was := handStoreHolds
handStoreHolds = func(context.Context, *inventory.Inventory, string) error { return nil }
t.Cleanup(func() { handStoreHolds = was })
set := flag.NewFlagSet("register", flag.ContinueOnError)
hand := addHandActFlags(set)
if err := set.Parse([]string{"--why", "the builder cannot build it"}); err != nil {
t.Fatal(err)
}
acts := func() []link.HandAct {
got, err := link.HandActs(ctx, b.js.Conn(), time.Now().Add(-time.Minute))
if err != nil {
t.Fatal(err)
}
return got
}
given := []string{"a=" + ref}
withTrunkList(t, false) // the list at the catalogue's trunk head cannot be read: the take-in waits
err = registerNow(ctx, open, keys, hand, "sudo", "novox/mesh-catalog", "modules/sudo", map[string]string{"a": ref}, given)
if err == nil || len(acts()) != 0 {
t.Fatalf("a take-in that waited: %v, and %d hand act(s)", err, len(acts()))
}
withTrunkList(t, true)
if err := registerNow(ctx, open, keys, hand, "sudo", "novox/mesh-catalog", "modules/sudo", map[string]string{"a": ref}, given); err != nil {
t.Fatal(err)
}
got := acts()
if len(got) != 1 || got[0].Verb != "register" || got[0].Why != "the builder cannot build it" {
t.Fatalf("the hand act of a registration made: %+v", got)
}
}
+9
View File
@@ -118,3 +118,12 @@ func TestAHandReferenceReplacesOneArtifactOfTheBuildersKey(t *testing.T) {
t.Errorf("Made: %v", got)
}
}
// C8b (ADR 0526 clause 3): a registration at references refuses without a why, before anything is opened or done.
func TestARegistrationAtReferencesRefusesWithoutAWhy(t *testing.T) {
ref := "artifact-store://x/tools/blobs/" + shaOf("t")
err := registerCommand(context.Background(), []string{"x", "someone/app", ".", "--reference", "tools=" + ref})
if err == nil || !strings.Contains(err.Error(), "--why") || !strings.Contains(err.Error(), "Nothing was done") {
t.Fatalf("a registration at references with no why: %v", err)
}
}