diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 128c683..769420c 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -331,6 +331,27 @@ type Have struct { Phase Phase `json:"phase"` Health Health `json:"health"` Detail string `json:"detail,omitempty"` + // UserList is what the node-engine of the node holding mesh-broker wrote of the bus's user list, in the have of the + // bus module (novox/hq ADR 0547 §7, §8). Nil on every other have. + UserList *UserListWritten `json:"user-list,omitempty"` +} + +// UserListWritten is the bus's user list as the node-engine of the bus node wrote it to the bus module's file, applied +// alone and with no want (novox/hq ADR 0547 §8): where it was taken from, what that source holds, and what the file +// holds now. The bus reloads the file itself when its digest changes (the module's entrypoint), so Digest is the list +// the bus loads. Wanted differs from Digest only while a write failed, and Error says why. +type UserListWritten struct { + // Source is the key the list was taken from: the controller's composition of the bus module for this node + // (`..`), or the grants part's `user-list` once it took over. + Source string `json:"source"` + Revision uint64 `json:"revision"` + // Wanted is the digest of the list the source holds, `sha256:`. + Wanted string `json:"wanted"` + // Digest is the digest of the file on the machine after the write, `sha256:`; empty when there is none. + Digest string `json:"digest,omitempty"` + Users int `json:"users"` + At time.Time `json:"at"` + Error string `json:"error,omitempty"` } // HaveNode is a node's own facts, written under HaveNodeKey. Each field is the node-engine's own JSON, carried whole: diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index 9d1ab21..3b1ac09 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -3,6 +3,7 @@ package delivery import ( "encoding/json" "errors" + "reflect" "strings" "testing" "time" @@ -149,6 +150,24 @@ func TestRoundTrip(t *testing.T) { t.Fatalf("got %+v want %+v", out, in) } }) + t.Run("have with the user list", func(t *testing.T) { + at := time.Date(2026, 10, 25, 21, 40, 0, 0, time.UTC) + in := Have{FP: "f1", Phase: PhaseRunning, Health: HealthOK, UserList: &UserListWritten{Source: "novox.nats.f1", + Revision: 7, Wanted: "sha256:ab", Digest: "sha256:ab", Users: 93, At: at}} + b, err := EncodeHave(in) + if err != nil { + t.Fatal(err) + } + hasV1(t, b) + out, err := DecodeHave(b) + if err != nil { + t.Fatal(err) + } + in.V = Version + if !reflect.DeepEqual(out, in) || !out.UserList.At.Equal(at) { + t.Fatalf("got %+v want %+v", out, in) + } + }) t.Run("have node", func(t *testing.T) { in := HaveNode{Windows: json.RawMessage(`{"a":1}`), Busy: json.RawMessage(`true`)} b, err := EncodeHaveNode(in)