Names N3 (view): the status view names its own buckets from its credential's module

Its states answers, reach and waits (viewstate WaitsBucket, ReachBucket, ViewAnswersBucket), its
first views and its marks were named from the constant ViewModule; they are now
`<cred.Module>_<state>` (viewstate.BucketsOf), and the model is made for its module.

Builds: design 47 G A-296

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 19:14:55 +01:00
parent e8d1bf96be
commit 0de534c692
22 changed files with 130 additions and 104 deletions
@@ -82,7 +82,7 @@ const (
tellerAnswers = "mesh-delivery-teller_answers"
tellerAsked = "mesh-delivery-teller_asked"
assignerAsks = "mesh-delivery-assigner_asks"
viewAnswers = viewstate.ViewAnswersBucket
viewAnswers = "mesh-delivery-view_" + viewstate.ViewAnswersState
tellerConsumer = "n1_mesh-delivery-teller"
seatAsk = "mesh.seat.operator-channel.accept.ask.mesh-delivery-teller"
seatCancel = "mesh.seat.operator-channel.accept.cancel.mesh-delivery-teller"
@@ -27,7 +27,7 @@ import (
func serve(t *testing.T) string {
t.Helper()
m := view.New()
m := view.New(viewstate.ViewModule)
placed, err := meshmoduleassignerclient.AssignmentsValues(meshmoduleassigner.Assignments{Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "x"}}})
if err != nil {
t.Fatal(err)
@@ -82,7 +82,7 @@ func TestFailsLoudWhenTheViewIsNotThere(t *testing.T) { // 1134/AC6
}
func TestPrintsTheCandidateGroupsAndTheFingerprintComparison(t *testing.T) { // 1241/AC6
m := view.New()
m := view.New(viewstate.ViewModule)
const head = "0123456789abcdef0123456789abcdef01234567"
cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{Group: "feat/x", Pulls: []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-delivery", Number: 12, Head: head, Modules: []string{"a"}}},
Heads: mustHash(t, meshdeliveryclient.Head{Repository: "novox/mesh-delivery", Number: 12, Head: head})})
@@ -130,7 +130,7 @@ func mustHash(t *testing.T, h meshdeliveryclient.Head) string {
// TestPrintsTheOpenAsksInTheTableAndTheJSON: the command shows what waits for the operator's Approve (1345/AC2). The ask is
// the gate's, as mesh-sdk go/delivery EncodeAsk writes it under the key MergeAskKey gives (as internal/view asks_test.go).
func TestPrintsTheOpenAsksInTheTableAndTheJSON(t *testing.T) { // 1345/AC2
m := view.New()
m := view.New(viewstate.ViewModule)
key, err := asks.HoldAskKey("plex", "src2:2222222222222222222222222222222222222222222222222222222222222222")
if err != nil {
t.Fatal(err)
@@ -219,7 +219,7 @@ func TestTheAnswerCommandAnswersAnAskNeedingNoProof(t *testing.T) { // 1297/B9b
if err != nil {
t.Fatal(err)
}
m, w := view.New(), &putter{puts: map[string][]byte{}}
m, w := view.New(viewstate.ViewModule), &putter{puts: map[string][]byte{}}
m.SetWriter(w)
m.Apply(view.Entry{Bucket: view.AssignerAsksBucket, Key: key, Value: value})
socket := serveWith(t, m)
@@ -262,7 +262,7 @@ func TestTheHoldCommandsListAndAnswerHolds(t *testing.T) {
hkey, _ := meshwantsclient.HeldKey("systemd-resolved", fp)
held, _ := meshwantsclient.EncodeHeld(meshwantsclient.Held{Node: "ace", Words: "health failed, phase running: dns timeout"})
ans, _ := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "keep", Warrant: hold.ID + "@2026-10-25T04:00:00Z", Words: "the operator chose keep"})
m, w := view.New(), &putter{puts: map[string][]byte{}}
m, w := view.New(viewstate.ViewModule), &putter{puts: map[string][]byte{}}
m.SetWriter(w)
m.Apply(view.Entry{Bucket: view.AssignerAsksBucket, Key: key, Value: value})
m.Apply(view.Entry{Bucket: view.AssignerWantsBucket, Key: hkey, Value: held})
@@ -4,13 +4,14 @@ import (
"context"
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"mesh-delivery-view/internal/view"
"sort"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
@@ -9,11 +9,12 @@ import (
"testing"
"time"
"mesh-delivery-view/internal/view"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"mesh-delivery-view/internal/view"
)
// encoded is v as its contract's encoder writes it, failing the test when it refuses.
@@ -564,7 +564,7 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129
return kv
}
askBucket := bucket(view.AssignerAsksBucket, view.AssignerAsksHistory)
viewBucket := bucket(viewstate.ViewAnswersBucket, viewstate.ViewAnswersHistory)
viewBucket := bucket(viewstate.BucketsOf(viewstate.ViewModule).Answers, viewstate.ViewAnswersHistory)
bucket(viewstate.ViewModule+"_"+views.MarksState, 1)
teller := bucket(view.AnswersBucket, view.AnswersHistory)
// The view says its answers on its views family (design 47 G A-178), which the teller follows.
@@ -788,7 +788,7 @@ func TestProcessWholeSaysTheSourceRevisionIsDelivered(t *testing.T) {
t.Fatal(err)
}
kvs := map[string]jetstream.KeyValue{}
for _, name := range []string{view.AssignerWantsBucket, view.HaveBucket, viewstate.ReachBucket} {
for _, name := range []string{view.AssignerWantsBucket, view.HaveBucket, viewstate.BucketsOf(viewstate.ViewModule).Reach} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: 5})
if err != nil {
t.Fatal(err)
@@ -860,7 +860,7 @@ func TestProcessWholeSaysTheSourceRevisionIsDelivered(t *testing.T) {
if err := json.Unmarshal(rm.Data, &re); err != nil || re.Key != "novox/web" {
t.Fatalf("the event reached = %s, %v", rm.Data, err)
}
if stored, gerr := kvs[viewstate.ReachBucket].Get(ctx, re.Key); gerr != nil || string(stored.Value()) != string(re.Reach) {
if stored, gerr := kvs[viewstate.BucketsOf(viewstate.ViewModule).Reach].Get(ctx, re.Key); gerr != nil || string(stored.Value()) != string(re.Reach) {
t.Errorf("the event carries %s, the state reach holds %v (%v)", re.Reach, stored, gerr)
}
}
@@ -66,7 +66,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
web = defaultWeb
}
model := view.New()
model := view.New(cred.Module)
own := viewstate.BucketsOf(cred.Module) // its own buckets, named from its credential's module (A-296)
l, err := view.ListenSocket(socket)
if err != nil {
return fmt.Errorf("cannot listen on %s: %w", socket, err)
@@ -81,7 +82,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return err
}
defer conn.Close()
model.SetWriter(kvWriter{ctx: ctx, js: js})
model.SetWriter(kvWriter{ctx: ctx, js: js, bucket: own.Answers})
model.SetMotionEvent(func(e view.MotionEvent) error {
err := emitEvent(ctx, js, cred, e.Type, e.Body, e.ID)
if err != nil {
@@ -106,7 +107,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return err
})
model.SetReachWriter(func(w view.ReachWrite) error {
rev, err := putReach(ctx, js, w)
rev, err := putReach(ctx, js, own.Reach, w)
if err != nil {
logger.Printf("the reach of %s could not be written (written again at the next change the view takes in): %v", w.Key, err)
return err
@@ -154,13 +155,13 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
}
open := buckets{js: js, reader: reader}
for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.AssignerAsksBucket, viewstate.ViewAnswersBucket, view.TakenBucket, viewstate.ReachBucket} {
for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.AssignerAsksBucket, own.Answers, view.TakenBucket, own.Reach} {
go follow(ctx, open, bucket, false, model, logger)
}
// The view's own state reach, as it stood at the start, is what the derivation lays its history over (design 47 G A-225);
// the modules' history is each revision of the catalog half the view takes, in its order (below), and each watched
// branch's source is the seat mesh-delivery's verb source per branch, followed by its source-moved (A-173).
statusBucket := process.AwaitBucket(ctx, js, conn, firstViewsBucket, "view_first_views reads nothing and view_first_view_again is not heard", logger.Printf)
statusBucket := process.AwaitBucket(ctx, js, conn, cred.Module+"_"+views.StatusState, "view_first_views reads nothing and view_first_view_again is not heard", logger.Printf)
// The modules are the catalog half's, per watched branch, through meshcatalogclient's branch follower adopted on the
// reader (design 47 G §532, §614): the registrations' branches, before the first registrations the gate's source's;
// each branch's first view, its status, its hand asks and its event catalog-revision are the reader's. A module's entry
@@ -174,7 +175,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
modules := &catalogModules{ctx: ctx, branches: branches, sources: sources, model: model, logger: logger, applied: map[string]bool{}}
branches.Changed = modules.changed
reader.Start(ctx, js, cred, statusBucket)
if err := hearWaits(ctx, hub, js, logger.Printf); err != nil {
if err := hearWaits(ctx, hub, js, own.Waits, logger.Printf); err != nil {
return fmt.Errorf("the waits' events are not handled: %w", err)
}
if err := hub.StartUntilBound(ctx, js, cred, nil, nil); err != nil {
@@ -207,10 +208,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// by view_read_answers and that family, never by reading the state. A watch that ends is sent on errs, and the program ends
// with it, loudly.
func sayAnswers(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, cred process.Account, writer views.WireWriter, errs chan<- error, logf func(string, ...any)) {
marks := viewstate.ViewModule + "_" + views.MarksState
marks := cred.Module + "_" + views.MarksState
process.AwaitBucket(ctx, js, nc, marks, "the view's answers are not said on its views family, so the teller waits on its first view", logf).Ready(func(mk jetstream.KeyValue) {
em := views.Emitter{Owner: viewstate.ViewModule, Subject: cred.EventSubject, Publish: views.JetStreamPublish(js), Marks: mk, Findings: writer, Logf: logf}
process.AwaitBucket(ctx, js, nc, viewstate.ViewAnswersBucket, "the view's answers are not said on its views family", logf).Ready(func(kv jetstream.KeyValue) {
process.AwaitBucket(ctx, js, nc, viewstate.BucketsOf(cred.Module).Answers, "the view's answers are not said on its views family", logf).Ready(func(kv jetstream.KeyValue) {
if err := em.Start(ctx, viewstate.ViewAnswersState, kv, errs); err != nil {
logf("%v: the teller asks view_read_answers at each change until the view starts again", err)
}
@@ -221,24 +222,25 @@ func sayAnswers(ctx context.Context, js jetstream.JetStream, nc process.Subscrib
// kvWriter is the view's one write: a key of its own bucket answers (manifest state entry answers), which the teller reads.
// The bucket is the controller's to create; a bucket not there yet is an error the operator is told.
type kvWriter struct {
ctx context.Context
js jetstream.JetStream
ctx context.Context
js jetstream.JetStream
bucket string
}
func (w kvWriter) Put(key string, value []byte) error {
kv, err := w.js.KeyValue(w.ctx, viewstate.ViewAnswersBucket)
kv, err := w.js.KeyValue(w.ctx, w.bucket)
if err != nil {
return fmt.Errorf("the bucket %s: %w", viewstate.ViewAnswersBucket, err)
return fmt.Errorf("the bucket %s: %w", w.bucket, err)
}
_, err = kv.Put(w.ctx, key, value)
return err
}
// putReach writes a reach value under its key in the view's own state reach (ADR 0556 clause 2), its only writer.
func putReach(ctx context.Context, js jetstream.JetStream, w view.ReachWrite) (uint64, error) {
kv, err := js.KeyValue(ctx, viewstate.ReachBucket)
func putReach(ctx context.Context, js jetstream.JetStream, bucket string, w view.ReachWrite) (uint64, error) {
kv, err := js.KeyValue(ctx, bucket)
if err != nil {
return 0, fmt.Errorf("the bucket %s: %w", viewstate.ReachBucket, err)
return 0, fmt.Errorf("the bucket %s: %w", bucket, err)
}
return kv.Put(ctx, w.Key, w.Value)
}
@@ -253,9 +255,6 @@ type ReachedEvent struct {
Reach json.RawMessage `json:"reach"`
}
// The view's own state of its first views (views.StatusState).
var firstViewsBucket = viewstate.ViewModule + "_" + views.StatusState
// ownersFollowed are the indexer's, the assigner's and the teller's states the view's process follows by their first views, each by its
// owner's read tool (as the view's invokes names it) and views family (its events on the shared events stream); the
// view's tools ask the other six through the owners' read tools on each call.
@@ -56,10 +56,10 @@ func applyWait(subject string, body []byte, put func(key string, value []byte) e
return fmt.Errorf("%s is no wait's subject", subject)
}
// hearWaits registers the waits' handlers on the process's hub; each keeps its event in the state waits and acknowledges it, and
// hearWaits registers the waits' handlers on the process's hub, the state waits its bucket; each keeps its event in the state waits and acknowledges it, and
// one that could not be kept is not acknowledged, so the bus gives it again.
func hearWaits(ctx context.Context, hub *process.EventHub, js jetstream.JetStream, logf func(string, ...any)) error {
kv := func() (jetstream.KeyValue, error) { return js.KeyValue(ctx, viewstate.WaitsBucket) }
func hearWaits(ctx context.Context, hub *process.EventHub, js jetstream.JetStream, bucket string, logf func(string, ...any)) error {
kv := func() (jetstream.KeyValue, error) { return js.KeyValue(ctx, bucket) }
put := func(key string, value []byte) error {
b, err := kv()
if err != nil {
@@ -53,7 +53,7 @@ func holdAsk(t *testing.T) (Entry, oc.Ask) {
}
func TestAnAskNeedingNoProofIsOpenInTheViewWithItsOptions(t *testing.T) { // 1297/B9
m := New()
m := New(viewstate.ViewModule)
e, a := holdAsk(t)
fill(m, e)
got := m.Snapshot().Asks
@@ -68,7 +68,7 @@ func TestAnAskNeedingNoProofIsOpenInTheViewWithItsOptions(t *testing.T) { // 129
}
func TestTheViewWritesItsAnswerUnderTheAsksViewKey(t *testing.T) { // 1297/B9a AC
m, w := New(), &memWriter{}
m, w := New(viewstate.ViewModule), &memWriter{}
m.SetWriter(w)
e, _ := holdAsk(t)
fill(m, e)
@@ -88,7 +88,7 @@ func TestTheViewWritesItsAnswerUnderTheAsksViewKey(t *testing.T) { // 1297/B9a A
t.Errorf("parse = %q, %v", back, err)
}
// The view hears its own answer back from its bucket and shows the ask as answered, waiting for the teller.
fill(m, Entry{Bucket: viewstate.ViewAnswersBucket, Key: e.Key + ".view", Value: raw, At: t0})
fill(m, Entry{Bucket: viewstate.BucketsOf(viewstate.ViewModule).Answers, Key: e.Key + ".view", Value: raw, At: t0})
got := m.Snapshot().Asks
if len(got) != 1 || got[0].Chosen != "release" {
t.Errorf("asks = %+v, want the ask with chosen release", got)
@@ -113,7 +113,7 @@ func TestTheViewRefusesWhatItMayNotAnswer(t *testing.T) { // 1297/B9a AC
{"no option", e.Key, " ", "no option", []Entry{e}},
}
for _, c := range cases {
m, w := New(), &memWriter{}
m, w := New(viewstate.ViewModule), &memWriter{}
m.SetWriter(w)
fill(m, c.prep...)
err := m.Answer(c.key, c.option, "")
@@ -124,7 +124,7 @@ func TestTheViewRefusesWhatItMayNotAnswer(t *testing.T) { // 1297/B9a AC
t.Errorf("%s: wrote %v", c.name, w.puts)
}
}
m := New()
m := New(viewstate.ViewModule)
e2, _ := holdAsk(t)
fill(m, e2)
if err := m.Answer(e2.Key, "release", ""); err == nil || !strings.Contains(err.Error(), "cannot write") {
@@ -182,7 +182,7 @@ func postAnswer(h http.Handler, form url.Values, mod func(*http.Request)) *httpt
}
func TestThePageAnswersAnAskAndRefusesAForeignPost(t *testing.T) { // 1297/B9a
m, w := New(), &memWriter{}
m, w := New(viewstate.ViewModule), &memWriter{}
m.SetWriter(w)
e, _ := holdAsk(t)
fill(m, e)
@@ -226,14 +226,14 @@ func TestThePageAnswersAnAskAndRefusesAForeignPost(t *testing.T) { // 1297/B9a
}
func TestAStaleViewAnswerOfAnotherAskingIsNotTheChoice(t *testing.T) { // 1297/B9 (stale answer)
m := New()
m := New(viewstate.ViewModule)
e, a := holdAsk(t)
fill(m, e)
stale, err := viewstate.EncodeViewAnswer(viewstate.ViewAnswer{Asking: "an-earlier-asking", Option: "release"})
if err != nil {
t.Fatal(err)
}
fill(m, Entry{Bucket: viewstate.ViewAnswersBucket, Key: e.Key + ".view", Value: stale, At: t0})
fill(m, Entry{Bucket: viewstate.BucketsOf(viewstate.ViewModule).Answers, Key: e.Key + ".view", Value: stale, At: t0})
if got := m.Snapshot().Asks; len(got) != 1 || got[0].Chosen != "" {
t.Errorf("a stale answer shows as the choice: %+v", got)
}
@@ -11,6 +11,7 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
)
// answerEntry is the teller's answer to the first asking of the key; answerToAsking to the asking with this router id. The
@@ -29,7 +30,7 @@ func answerToAsking(t *testing.T, key, id string) Entry {
}
func TestAnAskWithNoAnswerIsListedAndAnAnsweredOneIsNot(t *testing.T) { // 1345/AC1
m := New()
m := New(viewstate.ViewModule)
e, a := holdAsk(t)
fill(m, e)
s := m.Snapshot()
@@ -54,7 +55,7 @@ func TestAnAskWithNoAnswerIsListedAndAnAnsweredOneIsNot(t *testing.T) { // 1345/
// The gate's asks go with the Approve per merge (design 47 G §431, §512): a value under the gate's former state is no ask
// of the view's, and nothing names it unreadable.
func TestTheGatesMergeAsksAreNoAsksOfTheView(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
fill(m, Entry{Bucket: "mesh-delivery-gate_asks", Key: "merge.g.0123456789ab", Value: []byte(`{"v":1,"group":"g"}`), At: t0},
Entry{Bucket: "mesh-delivery-gate_asks", Key: "merge.h.0123456789ab", Value: []byte("{"), At: t0})
if s := m.Snapshot(); len(s.Asks) != 0 || len(s.Unreadable) != 0 {
@@ -63,7 +64,7 @@ func TestTheGatesMergeAsksAreNoAsksOfTheView(t *testing.T) {
}
func TestAnAnswerHeardBeforeItsAskStillHidesTheAsk(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
e, _ := holdAsk(t)
fill(m, answerEntry(t, e.Key), e)
if got := m.Snapshot().Asks; len(got) != 0 {
@@ -72,7 +73,7 @@ func TestAnAnswerHeardBeforeItsAskStillHidesTheAsk(t *testing.T) {
}
func TestAnUnreadableAskIsNamedAndNotListed(t *testing.T) { // never dropped silently
m := New()
m := New(viewstate.ViewModule)
e, _ := holdAsk(t)
fill(m, Entry{Bucket: AssignerAsksBucket, Key: e.Key, Value: []byte("{"), At: t0})
s := m.Snapshot()
@@ -82,7 +83,7 @@ func TestAnUnreadableAskIsNamedAndNotListed(t *testing.T) { // never dropped sil
}
func TestTableAndPageShowTheAsks(t *testing.T) { // 1345/AC2
m := New()
m := New(viewstate.ViewModule)
e, a := holdAsk(t)
fill(m, e)
var out bytes.Buffer
@@ -101,7 +102,7 @@ func TestTableAndPageShowTheAsks(t *testing.T) { // 1345/AC2
}
// No ask, no section.
out.Reset()
Table(&out, New().Snapshot())
Table(&out, New(viewstate.ViewModule).Snapshot())
if strings.Contains(out.String(), "ASKS") {
t.Errorf("the table shows an ASKS section with no ask:\n%s", out.String())
}
@@ -121,7 +122,7 @@ func TestTableAndPageShowTheAsks(t *testing.T) { // 1345/AC2
}
func TestAHaveOfARemovedModuleIsShownRemovedNotLeftBehind(t *testing.T) { // 1370: the node-engine writes phase removed
m := New()
m := New(viewstate.ViewModule)
fill(m, haveE(t, "novox", "mesh-delivery", have(t, "b1", nodeengineclient.PhaseRemoved, nodeengineclient.HealthOK, ""), t0),
haveE(t, "novox", "stray", have(t, "b2", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0))
s := m.Snapshot()
@@ -9,6 +9,7 @@ import (
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"github.com/nats-io/nats.go/jetstream"
)
@@ -53,7 +54,7 @@ func fakeSeats(t *testing.T, now time.Time) *Backlog {
func TestThreeWaitingAsksAreShownWithTheOldestsAge(t *testing.T) { // 1887/A8a AC
now := time.Date(2026, 10, 23, 12, 0, 0, 0, time.UTC)
m := New()
m := New(viewstate.ViewModule)
m.SetBacklog(fakeSeats(t, now))
rows := m.Snapshot().Backlog
var check, image *BacklogRow
@@ -88,7 +89,7 @@ func TestThreeWaitingAsksAreShownWithTheOldestsAge(t *testing.T) { // 1887/A8a A
}
func TestNoBacklogReaderGivesNoRows(t *testing.T) {
if got := New().Snapshot().Backlog; got == nil || len(got) != 0 {
if got := New(viewstate.ViewModule).Snapshot().Backlog; got == nil || len(got) != 0 {
t.Errorf("backlog = %#v", got)
}
}
@@ -6,6 +6,7 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
)
// I5 (issue 1851): from R4e the indexer writes the new form alone, and a module's fingerprint is built by the builder's key
@@ -27,7 +28,7 @@ func builderKey(t *testing.T, module, fp2 string, b builds.BuilderBuild) Entry {
}
func TestAFingerprintOfTheNewFormIsShownAgainstItsBuilderKey(t *testing.T) { // I5
m := New()
m := New(viewstate.ViewModule)
manifest := strings.Repeat("a", 64)
d := "sha256:" + strings.Repeat("2", 64)
fill(m,
@@ -12,6 +12,7 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
)
const (
@@ -66,7 +67,7 @@ func fpRow(t *testing.T, s Snapshot, module string) FingerprintRow {
}
func TestSnapshotListsTheCandidateGroups(t *testing.T) { // 1241/AC1
m := New()
m := New(viewstate.ViewModule)
fill(m,
candidate(t, "feat/x", pullOf("novox/mesh-delivery", 12, hA, "a", "b"), pullOf("novox/mesh-catalog", 300, hB, "c")),
candidate(t, "feat/y", pullOf("novox/mesh-delivery", 13, hB)))
@@ -94,7 +95,7 @@ func TestSnapshotListsTheCandidateGroups(t *testing.T) { // 1241/AC1
// 1955/L7 (ADR 0571 §1): the view reads no build record of the controller's: a module with no fingerprint of the new
// form is said so, never compared with the controller's builds.
func TestAModuleOfTheOldFormIsComparedWithNoBuild(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
fill(m,
modEntry(t, "a", meshcatalog.ModulesByModuleValue{Fp: fF, Repository: "novox/mesh-delivery", Commit: hA}),
modEntry(t, "b", meshcatalog.ModulesByModuleValue{None: "an artifact compiles a Go program from a build source that git trees do not pin: no fingerprint before release 4", Repository: "novox/mesh-delivery", Commit: hA}))
@@ -107,7 +108,7 @@ func TestAModuleOfTheOldFormIsComparedWithNoBuild(t *testing.T) {
}
func shown(t *testing.T) *Model {
m := New()
m := New(viewstate.ViewModule)
fill(m,
candidate(t, "feat/x", pullOf("novox/mesh-delivery", 12, hA, "a"), pullOf("novox/mesh-catalog", 300, hB, "c")),
modEntry(t, "a", meshcatalog.ModulesByModuleValue{Fp2: fp2A, Repository: "novox/mesh-delivery", Commit: hA}),
@@ -135,7 +136,7 @@ func TestTableAndPagePrintTheGroupsAndTheComparison(t *testing.T) { // 1241/AC6
}
func TestUnreadableIndexerValuesAreNamed(t *testing.T) { // never dropped silently
m := New()
m := New(viewstate.ViewModule)
fill(m, Entry{Bucket: CandidatesBucket, Key: "feat/x", Value: []byte("{"), At: t0},
Entry{Bucket: ModulesBucket, Key: "a", Value: []byte("{"), At: t0})
s := m.Snapshot()
@@ -36,7 +36,7 @@ func answered(t *testing.T, key, id, option string) Entry {
// 1906/N23-AC1: given holds in the assigner's wants, the view lists each with its node, its cause in words, its hold ask and
// the answer that stands for that asking; a hold without an ask says so, and a held key is never taken for a want.
func TestTheViewListsEveryHoldWithItsNodeCauseAndAnswer(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
e, a := holdAsk(t)
fill(m, e, heldE(t, "plex", fpNew, "ace", "health failed, phase running: connection refused"), answered(t, e.Key, a.ID, "keep"),
heldE(t, "web", fpNew, "g14", "health failed"))
@@ -60,7 +60,7 @@ func TestTheViewListsEveryHoldWithItsNodeCauseAndAnswer(t *testing.T) {
// writes its answer "release" to that asking under the ask's view key, though the ask was answered; the fingerprint may be the
// start of its hex. The same answer again, an unknown hold and another option are refused, and write nothing.
func TestAKeptHoldCanBeReleased(t *testing.T) {
m, w := New(), &memWriter{}
m, w := New(viewstate.ViewModule), &memWriter{}
m.SetWriter(w)
e, a := holdAsk(t)
fill(m, e, heldE(t, "plex", fpNew, "ace", "health failed"), answered(t, e.Key, a.ID, "keep"))
@@ -28,8 +28,9 @@ func ReachHistory(bucket string) string { return bucket + "#history" }
// reachBuckets are the watches the derivation waits for before it writes or says anything, so a start does not write a reach
// derived from half its inputs; the view's own reach first, which a restart lays the derivation over (A-225).
var reachBuckets = []string{ModulesBucket, AssignerWantsBucket, HaveBucket, TakenBucket, ReachHistory(ModulesBucket),
viewstate.ReachBucket}
func (m *Model) reachBuckets() []string {
return []string{ModulesBucket, AssignerWantsBucket, HaveBucket, TakenBucket, ReachHistory(ModulesBucket), m.own.Reach}
}
// ReachWrite is a reach value to write under its key in the view's state reach.
type ReachWrite struct {
@@ -380,7 +381,7 @@ func short(fp string) string {
// reachReady is whether every watch the derivation reads handed over what stood at its start.
func (m *Model) reachReady() bool {
for _, b := range reachBuckets {
for _, b := range m.reachBuckets() {
if !m.initial[b] {
return false
}
@@ -65,7 +65,7 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: viewstate.ReachBucket, Key: key, Value: v, At: t0}
return Entry{Bucket: viewstate.BucketsOf(viewstate.ViewModule).Reach, Key: key, Value: v, At: t0}
}
taken := func(repository, merge string) Entry {
key, _ := trunk.RepositoryKey(repository)
@@ -167,7 +167,7 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
then: []Entry{haveFP(t, "ace", "mail", fpMail, nodeengineclient.HealthOK)}},
} {
t.Run(tc.name, func(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
var written []ReachWrite
var said []DeliveredEvent
refuse := tc.refused
@@ -192,7 +192,7 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
if len(written) != 0 {
t.Fatalf("written before every watch handed over: %v", written)
}
for _, b := range reachBuckets {
for _, b := range m.reachBuckets() {
m.Initial(b)
}
refuse = false
@@ -12,11 +12,12 @@ import (
"testing"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
)
func served(t *testing.T) (string, *Model) {
t.Helper()
m := New()
m := New(viewstate.ViewModule)
fill(m, base(t)...)
fill(m, haveE(t, "a", "w", have(t, "b9", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0))
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "s")
@@ -113,7 +114,7 @@ func TestWebIsTheSameRowsAndReadOnly(t *testing.T) { // 1134/AC6
func watchmanSnapshot(t *testing.T) *Model {
t.Helper()
m := New()
m := New(viewstate.ViewModule)
fill(m, presenceE(t, "a", false, t0), stuckE(t, "node-engine_have", "a.x", "a report from a", t0, false),
Entry{Bucket: PresenceBucket, Key: "c", Value: []byte(`{"v":9}`), At: t0})
return m
@@ -236,6 +236,7 @@ type UnreadableRow struct {
// Model holds the rows' inputs in memory and nothing else: a restart fills it again from the buckets' last values.
type Model struct {
mu sync.Mutex
own viewstate.Buckets // its own buckets, named from its credential's module (A-296)
placed *meshmoduleassignerclient.Placed // the seat's assignments (the assigner's state, ADR 0567, ADR 0549 §3)
builds map[string]buildEntry // by <module>.build.<id>
groups map[string]groupEntry // the indexer's candidates, by group
@@ -364,9 +365,9 @@ type nodeEntry struct {
at time.Time
}
// New is an empty model.
func New() *Model {
return &Model{placed: meshmoduleassignerclient.NewPlaced(), builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, wants: map[string]wantEntry{}, told: map[string]bool{}, initial: map[string]bool{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]viewstate.ViewAnswer{}, answers: map[string]string{}, answerOf: map[string]meshdeliveryclient.Answer{}, holds: map[string]heldEntry{}, newest: map[string]string{},
// New is an empty model of the view module (its credential's).
func New(module string) *Model {
return &Model{own: viewstate.BucketsOf(module), placed: meshmoduleassignerclient.NewPlaced(), builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, wants: map[string]wantEntry{}, told: map[string]bool{}, initial: map[string]bool{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]viewstate.ViewAnswer{}, answers: map[string]string{}, answerOf: map[string]meshdeliveryclient.Answer{}, holds: map[string]heldEntry{}, newest: map[string]string{},
haves: map[string]haveEntry{}, nodes: map[string]nodeEntry{},
presence: map[string]presenceEntry{}, stuck: map[string]stuckEntry{},
modHist: map[string][]meshcatalog.ModulesByModuleValue{}, srcHist: map[string]map[uint64]map[string]string{}, kept: map[string]viewstate.Reach{},
@@ -582,7 +583,7 @@ func (m *Model) apply(e Entry) {
}
a, err := decodeAssignerAsk(e.Key, e.Value)
m.aasks[e.Key] = assignerAskRecord{ask: a, err: errText(err)}
case viewstate.ViewAnswersBucket:
case m.own.Answers: // the view's own state answers
askKey, err := viewstate.ParseViewAnswerKey(e.Key)
if err != nil {
return
@@ -640,8 +641,8 @@ func (m *Model) apply(e Entry) {
}
m.srcHist[e.Key][e.Revision] = src.Heads
}
case viewstate.ReachBucket: // the view's own state reach, as it stood at the start (A-225)
if m.initial[viewstate.ReachBucket] {
case m.own.Reach: // the view's own state reach, as it stood at the start (A-225)
if m.initial[m.own.Reach] {
return
}
if r, err := viewstate.DecodeReach(e.Value); err == nil && !e.Deleted {
@@ -18,6 +18,7 @@ import (
healers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/motion"
)
@@ -82,7 +83,7 @@ func base(t *testing.T) []Entry {
}
func TestRowsPerAssignedModuleAndNode(t *testing.T) { // 1134/AC1
m := New()
m := New(viewstate.ViewModule)
fill(m, base(t)...)
s := m.Snapshot()
if len(s.Rows) != 4 || s.Counted() != 4 {
@@ -101,14 +102,14 @@ func TestRowsPerAssignedModuleAndNode(t *testing.T) { // 1134/AC1
}
func TestLeftBehindIsWhatTheAssignmentsDoesNotPut(t *testing.T) { // 1134/AC3, issue 774 (the seat's assignments)
m := New()
m := New(viewstate.ViewModule)
fill(m, base(t)...)
fill(m, place(t, "b", "x")) // z taken off b
s := m.Snapshot()
if r := find(t, s, "b", "z"); r.Status != StatusLeftBehind || s.Counted() != 3 {
t.Errorf("status = %q counted %d, want %q and 3", r.Status, s.Counted(), StatusLeftBehind)
}
m = New()
m = New(viewstate.ViewModule)
fill(m, base(t)...)
fill(m, haveE(t, "a", "w", have(t, "b9", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0)) // placed nowhere
s = m.Snapshot()
@@ -122,7 +123,7 @@ func TestLeftBehindIsWhatTheAssignmentsDoesNotPut(t *testing.T) { // 1134/AC3, i
}
func TestUnknownVersionSaysSo(t *testing.T) { // 1134/AC4
m := New()
m := New(viewstate.ViewModule)
fill(m, base(t)...)
before := m.Snapshot()
fill(m, haveE(t, "a", "x", []byte(`{"v":9,"phase":"running","health":"ok"}`), t0))
@@ -146,7 +147,7 @@ func TestUnknownVersionSaysSo(t *testing.T) { // 1134/AC4
}
func TestOnlyTheAffectedRowChanges(t *testing.T) { // 1134/AC5
m := New()
m := New(viewstate.ViewModule)
fill(m, base(t)...)
before := m.Snapshot()
fill(m, haveE(t, "b", "x", have(t, "b1", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0.Add(9*time.Minute)))
@@ -174,7 +175,7 @@ func TestOnlyTheAffectedRowChanges(t *testing.T) { // 1134/AC5
t.Fatal("a deleted have must read as no have")
}
// a restart rebuilds the same rows from last values
a, b := New(), New()
a, b := New(viewstate.ViewModule), New(viewstate.ViewModule)
es := base(t)
fill(a, es...)
fill(b, es...)
@@ -184,7 +185,7 @@ func TestOnlyTheAffectedRowChanges(t *testing.T) { // 1134/AC5
}
func TestNodeRow(t *testing.T) { // 1134/AC1 (the node's own key gives a node row)
m := New()
m := New(viewstate.ViewModule)
v, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Windows: []map[string]any{{}, {}}, Busy: []map[string]any{{"a": 1}}, Strays: []map[string]any{}})
fill(m, Entry{Bucket: HaveBucket, Key: "a.node", Value: v, At: t0})
n := m.Snapshot().Nodes
@@ -218,7 +219,7 @@ func TestTheWatchmansKeysAndStatus(t *testing.T) {
}
func offlineAndStuckAreListed(t *testing.T) { // 1168/AC2
m := New()
m := New(viewstate.ViewModule)
fill(m, base(t)...)
fill(m,
presenceE(t, "a", false, t0),
@@ -246,7 +247,7 @@ func offlineAndStuckAreListed(t *testing.T) { // 1168/AC2
}
func unknownWatchmanVersionIsUnreadableNotDropped(t *testing.T) { // 1168/AC3
m := New()
m := New(viewstate.ViewModule)
fill(m,
Entry{Bucket: PresenceBucket, Key: "c", Value: []byte(`{"v":9,"online":false}`), At: t0},
Entry{Bucket: StuckBucket, Key: "node-engine_have.c.x", Value: []byte(`{"v":9}`), At: t0},
@@ -14,6 +14,7 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-sdk/go/keys"
)
@@ -87,7 +88,7 @@ func sourceRowOf(m *Model, branch string) *SourceRow {
// 1906/D7 AC7: every want is a row beside the have of its node, with where it stands.
func TestEveryWantIsARowAgainstItsHave(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
fill(m,
wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK),
wantE(t, "ace", "mail", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "mail", fpOld, nodeengineclient.HealthOK),
@@ -123,7 +124,7 @@ func TestEveryWantIsARowAgainstItsHave(t *testing.T) {
// 1906/D7 AC7: a source revision is delivered when every want equals its module's fingerprint and every have equals its
// want, healthy; one failed have shows it not delivered and names the module.
func TestASourceRevisionIsDerivedFromWantsAndHaves(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
var said []DeliveredEvent
m.SetDeliveredEvent(func(e DeliveredEvent) error {
if e.Repository == "" {
@@ -183,13 +184,13 @@ func TestASourceRevisionIsDerivedFromWantsAndHaves(t *testing.T) {
}
func TestNoSourceRevisionHeldMeansNoStateIsDerived(t *testing.T) {
if s := New().Snapshot().Sources; len(s) != 0 {
if s := New(viewstate.ViewModule).Snapshot().Sources; len(s) != 0 {
t.Errorf("source = %+v", s)
}
}
func TestTheWantsPageSaysWantsAgainstHaves(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "web", fpOld, nodeengineclient.HealthOK))
text := wantText(m.Snapshot())
if len(text) != 1 || text[0][0] != "ace" || text[0][1] != "web" || text[0][2] != fpNew || text[0][3] != fpOld || text[0][4] != WantPending || text[0][5] != "none" {
@@ -201,7 +202,7 @@ func TestTheWantsPageSaysWantsAgainstHaves(t *testing.T) {
func TestTheJudgementOfTheMoveIsShownBesideTheWant(t *testing.T) {
// 1955/L7 (ADR 0571 §6): the judgement is the node agent's, in its have waiting for a yes; the controller's composed copy
// is not read.
m := New()
m := New(viewstate.ViewModule)
h, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpOld, Phase: nodeengineclient.PhaseWaitingYes, Health: nodeengineclient.HealthOK,
Judgement: nodeengineclient.JudgedCannot, Waits: "sha256:" + strings.Repeat("c", 64), Detail: "the composition it runs is gone"})
if err != nil {
@@ -221,7 +222,7 @@ func TestTheJudgementOfTheMoveIsShownBesideTheWant(t *testing.T) {
// 1906/D9 (asked by the lead for issue 1946): the view says, once for each source revision, that it is delivered: the event the
// issue tracker derives "fix delivered" from, in place of the gate's states that go in release 6 (ADR 0539 §17).
func TestTheViewSaysOnceThatASourceRevisionIsDelivered(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
var said []DeliveredEvent
refuse := true // the bus refuses the first try to say it (issue 1126): it is said at the next change, not lost
m.SetDeliveredEvent(func(e DeliveredEvent) error {
@@ -267,7 +268,7 @@ func TestTheViewSaysOnceThatASourceRevisionIsDelivered(t *testing.T) {
// Review round 1 (view notes): the controller no longer writes the build of an assignment (C6), so a row reads the want and the
// have: the row shows the fingerprint wanted and waits while the node runs another.
func TestARowReadsTheWantWhenTheAssignmentNamesNoBuild(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
fill(m, place(t, "ace", "web"), wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "web", fpOld, nodeengineclient.HealthOK))
r := find(t, m.Snapshot(), "ace", "web")
if r.Assigned != fpNew || r.Have != fpOld || !strings.Contains(r.Waits, "has not applied") {
@@ -282,7 +283,7 @@ func TestARowReadsTheWantWhenTheAssignmentNamesNoBuild(t *testing.T) {
// Review round 1 (view notes, issue 1150): a module whose name holds a dot has its have under the escaped key; the row finds it,
// and a have nothing is assigned to keeps the module's name.
func TestADottedModuleNameIsFoundAndKept(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
// The seat's assignments refuses a dotted module name (mesh-sdk meshmoduleassigner.Assignments), so such a module is never placed:
// its have is a row left behind under its own name.
fill(m, place(t, "ace"), wantE(t, "ace", "novox.be", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "novox.be", fpNew, nodeengineclient.HealthOK),
@@ -320,7 +321,7 @@ func types(said []MotionEvent) string {
}
func TestAModuleInMotionIsSaidMovingThenSettledOnceEach(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
said := motionLog(m)
fill(m, haveFP(t, "ace", "web", fpOld, nodeengineclient.HealthOK))
if len(*said) != 0 {
@@ -364,7 +365,7 @@ func TestAModuleInMotionIsSaidMovingThenSettledOnceEach(t *testing.T) {
}
func TestAModuleHeldOrFailedIsSaidStuckWithItsWords(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
said := motionLog(m)
fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}))
b, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpOld, Phase: nodeengineclient.PhaseHeld, Health: nodeengineclient.HealthWaits, Detail: "no space left"})
@@ -377,7 +378,7 @@ func TestAModuleHeldOrFailedIsSaidStuckWithItsWords(t *testing.T) {
t.Errorf("stuck said twice: %q", types(*said))
}
// failed on the wanted fingerprint
m2 := New()
m2 := New(viewstate.ViewModule)
said2 := motionLog(m2)
fill(m2, wantE(t, "g14", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "g14", "web", fpNew, nodeengineclient.HealthFailed))
if types(*said2) != "moving,stuck" || (*said2)[1].Body.Outcome != "failed" {
@@ -386,7 +387,7 @@ func TestAModuleHeldOrFailedIsSaidStuckWithItsWords(t *testing.T) {
}
func TestAWantTheWatchmanSaysStoppedMovingIsSaidStuckPastItsBound(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
said := motionLog(m)
fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "web", fpOld, nodeengineclient.HealthOK))
sk, _ := meshwatchdogsclient.StuckKey(AssignerWantsBucket, "want.ace.web")
@@ -401,7 +402,7 @@ func TestAWantTheWatchmanSaysStoppedMovingIsSaidStuckPastItsBound(t *testing.T)
}
func TestANodeThatSettlesWithoutBeingSeenMovingSaysNothing(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
said := motionLog(m)
fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK), wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}))
if len(*said) != 0 {
@@ -412,7 +413,7 @@ func TestANodeThatSettlesWithoutBeingSeenMovingSaysNothing(t *testing.T) {
// Nothing is said of a module in motion before the buckets have been read: a want read ahead of its have is not a node behind. What is
// in motion when they have been read is said then, and a module that settles after is said settled.
func TestMotionIsSaidOnlyOnceTheBucketsHaveBeenRead(t *testing.T) {
m := New()
m := New(viewstate.ViewModule)
var said []MotionEvent
m.SetMotionEvent(func(e MotionEvent) error { said = append(said, e); return nil })
fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), wantE(t, "ace", "mail", meshwantsclient.Want{Fp: fpNew}))
+15 -3
View File
@@ -3,7 +3,6 @@ package manifest
import (
"bytes"
"encoding/json"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"io/fs"
"mesh-delivery-view/internal/view"
"os"
@@ -14,6 +13,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-sdk/go/keys"
@@ -256,8 +257,19 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
t.Errorf("the manifest names %s: the modules are the catalog half's (ADR 0663)", name)
}
}
if viewstate.ViewModule != m.Module || viewstate.ViewAnswersBucket != m.Module+"_answers" || viewstate.ReachBucket != m.Module+"_reach" {
t.Errorf("the view names itself %s (%s, %s), its manifest %s", viewstate.ViewModule, viewstate.ViewAnswersBucket, viewstate.ReachBucket, m.Module)
if viewstate.ViewModule != m.Module {
t.Errorf("the view names itself %s, its manifest %s", viewstate.ViewModule, m.Module)
}
// A-296: its own buckets are named from its credential's module, each a state its manifest keeps.
var own []string
for _, s := range m.State {
own = append(own, "a-view_"+s.Name)
}
b := viewstate.BucketsOf("a-view")
for _, want := range []string{b.Answers, b.Reach, b.Waits} {
if !slices.Contains(own, want) {
t.Errorf("the view keeps %s, its manifest's states are %v", want, own)
}
}
// The packages key is the seat's key of its state packages for a module (<module>), as the indexer writes it: the
// escaped path, with no dot, so a reader never takes it for a <package>.<series> key.
+12 -5
View File
@@ -22,12 +22,20 @@ const modulesHistory = 20
// How far a repository reached (novox/hq ADR 0556, issue 1280). The indexer says, per repository, the last merge it took in
// (the indexer's state `taken`); the delivery view keeps, per repository, how far each of its modules reached on the nodes that want it, and
// derives from it the repository's reached head (ReachBucket): the newest head of the repository at which every wanted
// derives from it the repository's reached head (its state reach): the newest head of the repository at which every wanted
// module runs, on every node, a build at or after its last change. A merge at or before the reached head is on the
// machines. Nothing records a merge as delivered (ADR 0429 §1): the gate and the tracker derive it from this state.
// ReachBucket is the delivery view's state `reach`: one key per repository (RepositoryKey), how far its modules reached.
const ReachBucket = ViewModule + "_reach"
// ReachState is the delivery view's state `reach`: one key per repository (RepositoryKey), how far its modules reached.
const ReachState = "reach"
// Buckets are the delivery view's own state buckets, `<module>_<state>`: waits, reach and answers.
type Buckets struct{ Waits, Reach, Answers string }
// BucketsOf names the view's own buckets from module, its credential's, never a constant (design 47 G A-296).
func BucketsOf(module string) Buckets {
return Buckets{Waits: module + "_" + WaitsState, Reach: module + "_" + ReachState, Answers: module + "_" + ViewAnswersState}
}
// Reach is the value of a reach key: how far the modules of one repository reached on the nodes. Indexed is the last
// merge the indexer took in for it (Taken.Merge), empty while it took in none. A repository the indexer holds no module
@@ -133,11 +141,10 @@ func SameReach(a, b Reach) bool {
return true
}
// ViewAnswersBucket is the delivery view's state `answers`: one key per ask it answered, and ViewAnswersHistory the revisions it
// ViewAnswersState is the delivery view's state `answers`: one key per ask it answered, and ViewAnswersHistory the revisions it
// keeps per key.
const (
ViewAnswersState = "answers"
ViewAnswersBucket = ViewModule + "_" + ViewAnswersState
ViewAnswersHistory = 5
// ViewAnswersReadTool is the view's read tool of its state answers (mesh-sdk views.ReadTool), whose changes it says on its
// views family as views.answers; its marks are its state view-marks (views.MarksState) (design 47 G A-178).
@@ -15,9 +15,6 @@ import (
// `wait-ended`, and read by view_tiers through the view's own read tool.
const WaitsState = "waits"
// WaitsBucket is the bus's bucket of the state waits.
const WaitsBucket = ViewModule + "_waits"
// Wait is the value of a waits key: the fields of the seat mesh-wants' wait (§498, §512, §629) as the part that holds it said
// them. The view writes no words of its own: Words is the sayer's.
type Wait struct {