From ce286795e0183d67f1bb8e883fcaed67e53a0ecd Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 12:17:16 +0100 Subject: [PATCH] CT10: a branch with no revision yet is a ready, empty catalog view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §479, §495 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../go/clients/meshcatalogclient/catalog.go | 15 +++++++++++++++ .../meshcatalogclient/catalog_view_test.go | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/contracts/go/clients/meshcatalogclient/catalog.go b/contracts/go/clients/meshcatalogclient/catalog.go index 95d46cf5..95403eb4 100644 --- a/contracts/go/clients/meshcatalogclient/catalog.go +++ b/contracts/go/clients/meshcatalogclient/catalog.go @@ -3,9 +3,11 @@ package meshcatalogclient import ( "context" "encoding/json" + "errors" "fmt" "maps" "strconv" + "strings" "sync" "time" @@ -174,6 +176,10 @@ func (c *Catalog) ask(_ context.Context, after string) (views.Answer, error) { } c.mu.Unlock() a, err := askRevision(c.call, in) + if errors.Is(err, errNoneYet) { + // A branch with no revision yet is ready and empty, and nothing waits for it (§479, §495). + return views.Answer{V: 1, Owner: seat.Seat, State: c.Follower.State, Created: catalogCreated, Values: map[string]json.RawMessage{}, At: time.Now()}, nil + } if err != nil { return views.Answer{}, err } @@ -210,13 +216,22 @@ func (c *Catalog) ask(_ context.Context, after string) (views.Answer, error) { return views.Answer{V: 1, Owner: seat.Seat, State: c.Follower.State, Revision: uint64(rev), Created: catalogCreated, Values: values, Next: a.Next, At: time.Now()}, nil } +// errNoneYet is the seat's answer `none yet`: the branch has no revision yet. +var errNoneYet = errors.New("none yet") + func askRevision(call Call, in seat.RevisionInput) (seat.RevisionAnswer, error) { raw, err := call(RevisionVerb, in) if err != nil { + if strings.Contains(err.Error(), views.NoneYet) { + return seat.RevisionAnswer{}, errNoneYet + } return seat.RevisionAnswer{}, err } answer, said, ok := buscalls.Unwrap(raw) if !ok { + if strings.Contains(said, views.NoneYet) { + return seat.RevisionAnswer{}, errNoneYet + } return seat.RevisionAnswer{}, fmt.Errorf("%s answered: %s", RevisionVerb, said) } var a seat.RevisionAnswer diff --git a/contracts/go/clients/meshcatalogclient/catalog_view_test.go b/contracts/go/clients/meshcatalogclient/catalog_view_test.go index 6077fede..7af15ae0 100644 --- a/contracts/go/clients/meshcatalogclient/catalog_view_test.go +++ b/contracts/go/clients/meshcatalogclient/catalog_view_test.go @@ -27,6 +27,9 @@ func (f *fakeRevisions) call(key string, body any) (json.RawMessage, error) { f.mu.Lock() defer f.mu.Unlock() rev, ok := f.newest[in.Branch] + if ok && rev == 0 { + return nil, fmt.Errorf("revision of %s: none yet", in.Branch) + } if !ok { return nil, fmt.Errorf("asked for the branch %q", in.Branch) } @@ -136,6 +139,20 @@ func TestCatalogIsAViewPerWatchedBranch(t *testing.T) { t.Errorf("the gap was not said in the log: %q", logged) } + // A branch with no revision yet is ready and empty, and nothing waits for it. + f.set("new", 0) + none, err := NewCatalog(f.call, "new") + if err != nil { + t.Fatal(err) + } + if err := none.Start(ctx); err != nil { + t.Fatal(err) + } + <-none.Ready() + if _, ok := none.View(); ok || none.Follower.Status().WaitsFor != "" { + t.Errorf("a branch with no revision yet holds a view or waits: %+v", none.Follower.Status()) + } + for _, data := range []string{`{}`, `not json`} { if err := main.Hear(ctx, []byte(data)); err == nil { t.Errorf("Hear(%s) took it", data)