From eaa903a8ec2b817a9cb66118ce93240a920180ea Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 13 Nov 2026 20:43:21 +0100 Subject: [PATCH] S4 catalog-ready-on-a-fresh-mesh: with the registrations and the gate's source answered and naming no branch, Ready is nil and the view is empty Builds: design 47 G A-368 (G-22-lead-answers-2.md), start-waits.md S4 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../go/clients/meshcatalogclient/entry.go | 30 +++++++++++++- .../clients/meshcatalogclient/entry_test.go | 41 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/modules/mesh-delivery-contracts/go/clients/meshcatalogclient/entry.go b/modules/mesh-delivery-contracts/go/clients/meshcatalogclient/entry.go index 19e6ff16..3d9b1eeb 100644 --- a/modules/mesh-delivery-contracts/go/clients/meshcatalogclient/entry.go +++ b/modules/mesh-delivery-contracts/go/clients/meshcatalogclient/entry.go @@ -12,10 +12,16 @@ import ( ) // Ready is nil once every watched branch has its first view (`none-yet` counting as one), else says what it waits for -// (design 47 G §614, A-103): a part acts on nothing until then. +// (design 47 G §614, A-103): a part acts on nothing until then. On a fresh mesh, where the registrations and the gate's source are both +// answered and name no branch, it is nil too: the trunk is `none-yet` (A-368). func (b *Branches) Ready() error { followed := b.Followed() if len(followed) == 0 { + // A fresh mesh: the registrations and the gate's source are both answered and name no branch, so the trunk is `none-yet` + // and the view is empty (design 47 G, A-368). + if b.namesNoBranch() { + return nil + } return fmt.Errorf("the catalog half names no watched branch yet (the registrations of the seat %s are asked until they answer)", seat.Seat) } var waiting []string @@ -34,6 +40,28 @@ func (b *Branches) Ready() error { return nil } +// namesNoBranch says the first view of the registrations is in and, while the gate's source is followed, so is its, and +// neither names a branch (none is followed). +func (b *Branches) namesNoBranch() bool { + select { + case <-b.Registrations.Ready(): + default: + return false + } + b.mu.Lock() + s := b.source + b.mu.Unlock() + if s == nil { + return true + } + select { + case <-s.Follower.Ready(): + return true + default: + return false + } +} + // Entry is a module's entry at the trunk: the module's own registration decides, so the branch followed first, which lists // the module, is the one read (design 47 G A-103). An error says a watched branch has no first view yet, never "no module". func (b *Branches) Entry(module string) (seat.CatalogEntry, bool, error) { diff --git a/modules/mesh-delivery-contracts/go/clients/meshcatalogclient/entry_test.go b/modules/mesh-delivery-contracts/go/clients/meshcatalogclient/entry_test.go index 3a92d0a5..d0a2078d 100644 --- a/modules/mesh-delivery-contracts/go/clients/meshcatalogclient/entry_test.go +++ b/modules/mesh-delivery-contracts/go/clients/meshcatalogclient/entry_test.go @@ -94,3 +94,44 @@ func TestTheBranchFollowerGivesEveryPartItsEntryHelpers(t *testing.T) { t.Error("a released entry kept its finding") } } + +// A-368 (design 47 G, C6): on a fresh mesh, where neither the registrations nor the gate's source name a branch, the catalog half +// counts the trunk as `none-yet`: Ready is nil once both are answered, and the view is empty (Entry finds no module); until +// both answer, no part is ready. +func TestAMeshThatNamesNoBranchIsReadyWithAnEmptyView(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + for _, c := range []struct { + name string + start []string // the followers started, by the name they are adopted under + ready bool + }{ + {"nothing answered", nil, false}, + {"only the registrations answered", []string{"mesh-catalog.registrations"}, false}, + {"only the gate's source answered", []string{"mesh-delivery.source"}, false}, + {"both answered", []string{"mesh-catalog.registrations", "mesh-delivery.source"}, true}, + } { + t.Run(c.name, func(t *testing.T) { + seats := &fakeSeats{regs: map[string][]string{}} + w := &fakeWiring{adopted: map[string]*views.Follower{}, hears: map[string]func(context.Context, []byte) error{}} + b, err := NewBranches(w, seats.call) + if err != nil { + t.Fatal(err) + } + for _, name := range c.start { + f, _ := w.get(name) + if err := f.Start(ctx); err != nil { + t.Fatal(err) + } + } + if err := b.Ready(); (err == nil) != c.ready { + t.Fatalf("Ready = %v, want ready %v", err, c.ready) + } + if c.ready { + if e, found, err := b.Entry("m"); err != nil || found { + t.Errorf("Entry = %v, %v, %v: want an empty view, no error", e, found, err) + } + } + }) + } +}