diff --git a/node-tools/go.mod b/node-tools/go.mod index 69cda1a..650042f 100644 --- a/node-tools/go.mod +++ b/node-tools/go.mod @@ -8,7 +8,7 @@ require ( ) require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030142317-46d4b4da5ebb + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030143623-83cf97b690f5 github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/klauspost/compress v1.20.0 // indirect diff --git a/node-tools/go.sum b/node-tools/go.sum index 80f4477..1b0193a 100644 --- a/node-tools/go.sum +++ b/node-tools/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030142317-46d4b4da5ebb h1:QvNKoCjW1cwKsaY/9iJO0srTFIBQQwEZMkjb664BAPg= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030142317-46d4b4da5ebb/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030143623-83cf97b690f5 h1:hyyaB1rU7uzlQZvWAxZ9P6pL/4LCFpOVXMRgn9F7fic= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030143623-83cf97b690f5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/node-tools/internal/bus/bus.go b/node-tools/internal/bus/bus.go index fc4b508..ee01017 100644 --- a/node-tools/internal/bus/bus.go +++ b/node-tools/internal/bus/bus.go @@ -470,8 +470,9 @@ func (c *Conn) followGrants(module string) { return // a membership withdrawn is the stream's, or the module's unassignment's, to say } // Read through mesh-sdk's one codec of this state, the grants part's writer's (issue 774): it takes the bare - // membership and the wrapper the part wrote before, and refuses a value naming no node or module, which then - // leaves the assignments stream's copy in force instead of an empty membership. + // membership and a wrapper the part wrote before of one published or unchanged, and refuses a held-back one's + // wrapper or a value naming no node or module, which then leaves the assignments stream's copy in force instead. + // A refused value arriving after a good one keeps the last good membership: nothing here clears it. var m Membership body, err := membership.BareGrantsMembership(e.Value()) if err == nil { diff --git a/node-tools/internal/bus/state_watch_test.go b/node-tools/internal/bus/state_watch_test.go index 61166b4..5ca378e 100644 --- a/node-tools/internal/bus/state_watch_test.go +++ b/node-tools/internal/bus/state_watch_test.go @@ -125,7 +125,13 @@ func TestAMembershipTheGrantsPartWroteWrappedIsServed(t *testing.T) { mesh.Bucket(t, "gitea_repos") gitea := mt.MembershipOf("gitea", "anchor", false, nil) gitea.State = []bus.StateIssued{{Name: "repos", Bucket: "gitea_repos", Writes: true}} - mesh.IssueInGrantsWrapped(t, gitea) + mesh.IssueInGrantsWrapped(t, gitea, "published") + // The old wrapper of a membership the part held back carries its unpublished composition: refused, so the + // assignments stream's copy is served, never what the hold kept back. + pages := mt.MembershipOf("pages", "anchor", false, nil) + mesh.Issue(t, pages) + heldBack := mt.MembershipOf("pages", "anchor", true, nil) + mesh.IssueInGrantsWrapped(t, heldBack, "not published, a shadow difference: publishing would lose the last holder of x") conn, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: "node-tools", Node: "anchor"}) if err != nil { t.Fatal(err) @@ -148,6 +154,16 @@ func TestAMembershipTheGrantsPartWroteWrappedIsServed(t *testing.T) { if e, err := conn.StateGet("gitea", "repos", "novox.hq"); err != nil || e == nil || string(e.Value) != `{"v":1}` { t.Errorf("the state reads back %+v, %v", e, err) } + conn.Follow("pages") + mt.Until(t, func() error { + if m := conn.Membership("pages"); m == nil || len(m.Serves) == 0 { + return fmt.Errorf("pages' membership is %+v, want the assignments stream's", m) + } + return nil + }) + if got, want := fmt.Sprint(conn.ServedOn("pages", "pages_list")), "[{mesh.mod.pages.tool.pages_list.anchor }]"; got != want { + t.Errorf("a held-back membership's wrapper is served: pages_list on %s, want the stream's %s", got, want) + } } // owedWatcher is the gitea module's view of the checker's owed: the bucket, and a runtime connection diff --git a/node-tools/internal/meshtest/meshtest.go b/node-tools/internal/meshtest/meshtest.go index a22fd93..4d9f5d9 100644 --- a/node-tools/internal/meshtest/meshtest.go +++ b/node-tools/internal/meshtest/meshtest.go @@ -119,11 +119,12 @@ func (m *Mesh) IssueInGrants(t *testing.T, mem bus.Membership) { } // IssueInGrantsWrapped writes a membership as the grants part wrote it before mesh-sdk's codec (until 2026-10-30, what -// the live mesh holds until the part writes again): its pass record, the membership inside. -func (m *Mesh) IssueInGrantsWrapped(t *testing.T, mem bus.Membership) { +// the live mesh holds until the part writes again): its pass record, the membership inside, published saying what +// became of it ("published", "unchanged", or why it was held back). +func (m *Mesh) IssueInGrantsWrapped(t *testing.T, mem bus.Membership, published string) { t.Helper() body, _ := json.Marshal(map[string]any{"node": mem.Node, "module": mem.Module, "digest": "sha256:0", - "membership": sdkMembership(t, mem), "published": "published"}) + "membership": sdkMembership(t, mem), "published": published}) m.putGrants(t, mem, body) }