From e1fd3eb0f660aab97a698ada81ff1602bd771890 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 29 Oct 2026 21:52:42 +0100 Subject: [PATCH] View: answer status from the buckets it reads and derive motion with mesh-sdk's MotionOf, so status leaves the controller and the keeper judges a delivery as the view does (issue 1952 V1) --- .../cmd/mesh-delivery-view-tools/main.go | 17 +- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 +- .../mesh-delivery-view/internal/view/read.go | 10 + .../internal/view/status.go | 218 ++++++++++++++++++ .../mesh-delivery-view/internal/view/view.go | 35 ++- .../internal/view/view_test.go | 79 ++++++- modules/mesh-delivery-view/manifest_test.go | 4 +- modules/mesh-delivery-view/module.json | 3 + 9 files changed, 341 insertions(+), 31 deletions(-) create mode 100644 modules/mesh-delivery-view/internal/view/status.go diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go index 55721c7..3abc440 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "strings" + "time" "mesh-delivery-view/internal/view" @@ -19,6 +20,14 @@ import ( // Tools are the read tools over a modules state, and build_usage over the build modules' states. func Tools(r view.Reader, u view.UsageReader) []stdio.Tool { return []stdio.Tool{ + {Name: view.StatusTool, + Description: "The mesh's status, derived by the view from the buckets it reads (novox/hq ADR 0564 §10), nothing written as a " + + "summary: every open condition of the condition keeper (mesh-conditions), urgent first then oldest; what is quiet (each " + + "node the watchman says is offline, each key it says stopped moving); each module whose want and have differ on a node " + + "(moving, or stuck with the have's or the watchman's words), as mesh-sdk's delivery motion derives it; and what " + + "mesh-healers did in the last seven days, by outcome. A part that cannot be read says so, never as none; well is true " + + "only when nothing is open, quiet or stuck and every part was read. Read-only.", + Run: func(map[string]any) (any, error) { return r.Status(time.Now()), nil }}, {Name: view.ReachReadTool, Description: "Whether a merge is on the machines: per repository, the reached head (the newest merge every node runs a build " + "containing, as the view derives it from the indexer's modules, the wants and the haves; a merge at or before it is on the " + @@ -232,9 +241,11 @@ func read(state stdio.KeptState) view.StateRead { func main() { r := view.Reader{State: read(stdio.State("mesh-delivery-indexer.modules")), Builds: read(stdio.State("mesh-delivery-builder.builds")), CandidatesState: read(stdio.State("mesh-delivery-indexer.candidates")), WantsState: read(stdio.State("mesh-delivery-assigner.wants")), DifferencesState: read(stdio.State("mesh-delivery-assigner.differences")), HavesState: read(stdio.State("node-engine.have")), - ReachState: read(stdio.State("reach")), - HeardState: read(stdio.State("mesh-delivery-indexer.heard")), - SignersState: read(stdio.State("mesh-vault.signers"))} + ReachState: read(stdio.State("reach")), + HeardState: read(stdio.State("mesh-delivery-indexer.heard")), + SignersState: read(stdio.State("mesh-vault.signers")), + ConditionsState: read(stdio.State(view.StatusConditionsState)), AttemptsState: read(stdio.State(view.StatusAttemptsState)), + PresenceState: read(stdio.State(view.StatusPresenceState)), StuckState: read(stdio.State(view.StatusStuckState))} u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")), Steps: map[string]view.StateRead{}, Pauses: map[string]view.StateRead{}, Backlog: backlogOf(socket())} for _, lang := range delivery.AskLanguages { diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 537359b..82a7256 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029200316-22c9ddd32b6a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 043e05f..ae67790 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029200316-22c9ddd32b6a h1:nhRFmtdcI4VkghGUdJhpt1BnPuTQcekioOIRzW8fcG0= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029200316-22c9ddd32b6a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba h1:uXwkcQWgbu/UthizGHFaKPiA4NBVJgqL9q2jJnnGR1o= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba/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/modules/mesh-delivery-view/internal/view/read.go b/modules/mesh-delivery-view/internal/view/read.go index 9fdbe2f..58e45a3 100644 --- a/modules/mesh-delivery-view/internal/view/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -12,6 +12,7 @@ import ( "strings" "sync" "sync/atomic" + "time" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -46,6 +47,15 @@ type Reader struct { HeardState StateRead // SignersState is the vault's public signing halves (ADR 0550 §2); unset reads none. SignersState StateRead + // ConditionsState, AttemptsState, PresenceState and StuckState are what status reads beside the wants and the haves + // (ADR 0564 §10): the condition keeper's open conditions, mesh-healers' attempts, the watchman's presence and stuck keys. + ConditionsState, AttemptsState, PresenceState, StuckState StateRead +} + +// Status is the view's derivation of status at now (status.go). +func (r Reader) Status(now time.Time) Status { + return DeriveStatus(StatusRead{Conditions: r.ConditionsState, Attempts: r.AttemptsState, Presence: r.PresenceState, + Stuck: r.StuckState, Wants: r.WantsState, Haves: r.HavesState}, now) } // ReachReadTool reads the view's reach: per repository, how far each module reached and the reached head (ADR 0556). diff --git a/modules/mesh-delivery-view/internal/view/status.go b/modules/mesh-delivery-view/internal/view/status.go new file mode 100644 index 0000000..d8a0837 --- /dev/null +++ b/modules/mesh-delivery-view/internal/view/status.go @@ -0,0 +1,218 @@ +package view + +import ( + "encoding/json" + "fmt" + "sort" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/health" +) + +// `status` is the view's derivation (novox/hq ADR 0564 §10, Limits 7, issue 1952 V1): what is wrong, quiet and on its way, +// read from the buckets it watches, never from a summary someone writes (research 072 §2), so the controller's status, its +// summary and the probe D9 go with its lease. Each part comes from one bucket: +// - Conditions: the condition keeper's open conditions (mesh-conditions.conditions), urgent first, then oldest; +// - Quiet: the watchman's presence of each node that is offline, and its stuck keys not moved; +// - Moving: each module whose want and have differ, as mesh-sdk's delivery.MotionOf derives it (moving or stuck); +// - Heals: what mesh-healers did in the last seven days (mesh-healers.attempts), by outcome. +// A bucket that cannot be read says so in its part's Unread, never as none. + +// StatusTool is the view's tool that answers status. +const StatusTool = "view_status" + +// The states status reads, as the view's manifest names them. +const ( + StatusConditionsState = health.KeeperModule + "." + health.ConditionsState + StatusAttemptsState = health.HealersModule + "." + health.AttemptsState + StatusPresenceState = delivery.WatchmanModule + ".presence" + StatusStuckState = delivery.WatchmanModule + ".stuck" +) + +// HealsWithin is how far back status counts the healers' acts. +const HealsWithin = 7 * 24 * time.Hour + +// StatusRead is the five buckets status reads, as a tool reads them. +type StatusRead struct { + Conditions, Attempts, Presence, Stuck, Wants, Haves StateRead +} + +// Status is the view's answer to status. +type Status struct { + Conditions []StatusCondition `json:"conditions"` + ConditionsUnread string `json:"conditions-unread,omitempty"` + Quiet []StatusQuiet `json:"quiet"` + QuietUnread string `json:"quiet-unread,omitempty"` + Moving []StatusMoving `json:"moving"` + MovingUnread string `json:"moving-unread,omitempty"` + Heals map[string]int `json:"heals-this-week"` + HealsUnread string `json:"heals-unread,omitempty"` + // Well says nothing is open, quiet or stuck; a part unread is not well. + Well bool `json:"well"` +} + +// StatusCondition is one open condition in brief. +type StatusCondition struct { + Key string `json:"key"` + Severity string `json:"severity"` + Machine string `json:"machine,omitempty"` + Summary string `json:"summary"` + Raised time.Time `json:"raised"` + Silenced bool `json:"silenced,omitempty"` +} + +// StatusQuiet is a node offline, or a key the watchman says stopped moving. +type StatusQuiet struct { + Key string `json:"key"` + What string `json:"what"` + Since time.Time `json:"since,omitzero"` +} + +// StatusMoving is a module whose want and have differ on a node. +type StatusMoving struct { + Node string `json:"node"` + Module string `json:"module"` + State string `json:"state"` + Outcome string `json:"outcome,omitempty"` + Words string `json:"words,omitempty"` + FromFP string `json:"from_fp,omitempty"` + ToFP string `json:"to_fp"` +} + +func values(s StateRead) (map[string]json.RawMessage, error) { + if s.Keys == nil || s.Get == nil { + return nil, fmt.Errorf("not read here") + } + keys, err := s.Keys() + if err != nil { + return nil, err + } + out := map[string]json.RawMessage{} + for _, k := range keys { + v, err := s.Get(k) + if err != nil { + return nil, fmt.Errorf("%s: %w", k, err) + } + if v != nil { + out[k] = v + } + } + return out, nil +} + +// DeriveStatus is status from the buckets r reads, at now. +func DeriveStatus(r StatusRead, now time.Time) Status { + st := Status{Conditions: []StatusCondition{}, Quiet: []StatusQuiet{}, Moving: []StatusMoving{}, Heals: map[string]int{}} + + if raw, err := values(r.Conditions); err != nil { + st.ConditionsUnread = "the condition keeper's conditions cannot be read: " + err.Error() + } else { + var open []health.Condition + for _, v := range raw { + var c health.Condition + if json.Unmarshal(v, &c) == nil && c.Key != "" { + open = append(open, c) + } + } + health.Order(open) + for _, c := range open { + st.Conditions = append(st.Conditions, StatusCondition{Key: c.Key, Severity: string(c.Severity), Machine: c.Subject.Machine, + Summary: c.Summary, Raised: c.Raised, Silenced: c.SilencedAt(now)}) + } + } + + presence, perr := values(r.Presence) + stuck, serr := values(r.Stuck) + switch { + case perr != nil: + st.QuietUnread = "the watchman's presence cannot be read: " + perr.Error() + case serr != nil: + st.QuietUnread = "the watchman's stuck keys cannot be read: " + serr.Error() + default: + for k, v := range presence { + if p, err := delivery.DecodePresence(v); err == nil && !p.Online { + st.Quiet = append(st.Quiet, StatusQuiet{Key: k, What: "offline"}) + } + } + for k, v := range stuck { + if s, err := delivery.DecodeStuck(v); err == nil && !s.Moved { + st.Quiet = append(st.Quiet, StatusQuiet{Key: k, What: "stopped moving: it waited for " + s.Waited, Since: s.Since}) + } + } + sort.Slice(st.Quiet, func(i, j int) bool { return st.Quiet[i].Key < st.Quiet[j].Key }) + } + + wants, werr := values(r.Wants) + haves, herr := values(r.Haves) + switch { + case werr != nil: + st.MovingUnread = "the assigner's wants cannot be read: " + werr.Error() + case herr != nil: + st.MovingUnread = "the nodes' haves cannot be read: " + herr.Error() + default: + for wk, v := range wants { + node, module, err := delivery.ParseWantKey(wk) + if err != nil { + continue + } + w, err := delivery.DecodeWant(v) + if err != nil { + continue + } + hk, _ := delivery.HaveKey(node, module) + var h delivery.Have + hv, has := haves[hk] + if has { + if h, err = delivery.DecodeHave(hv); err != nil { + has = false + } + } + words, stopped := "", false + if sk, err := delivery.StuckKey(delivery.AssignerWantsBucket, wk); err == nil && serr == nil { + if s, err := delivery.DecodeStuck(stuck[sk]); err == nil && !s.Moved { + words, stopped = s.Waited, true + } + } + m := delivery.MotionOf(w, true, h, has, words, stopped) + if m.State == "" || m.State == delivery.MotionSettled { + continue + } + row := StatusMoving{Node: node, Module: module, State: m.State, Outcome: m.Outcome, Words: m.Words, ToFP: w.FP} + if has { + row.FromFP = h.FP + } + st.Moving = append(st.Moving, row) + } + sort.Slice(st.Moving, func(i, j int) bool { + if st.Moving[i].Node != st.Moving[j].Node { + return st.Moving[i].Node < st.Moving[j].Node + } + return st.Moving[i].Module < st.Moving[j].Module + }) + } + + if raw, err := values(r.Attempts); err != nil { + st.HealsUnread = "mesh-healers' attempts cannot be read: " + err.Error() + } else { + for _, v := range raw { + var a health.Attempt + if json.Unmarshal(v, &a) != nil || now.Sub(a.Begun) > HealsWithin { + continue + } + outcome := a.Outcome + if outcome == "" { + outcome = health.OutcomeActing + } + st.Heals[outcome]++ + } + } + + stuckMoving := false + for _, m := range st.Moving { + stuckMoving = stuckMoving || m.State == delivery.MotionStuck + } + st.Well = len(st.Conditions) == 0 && len(st.Quiet) == 0 && !stuckMoving && + st.ConditionsUnread == "" && st.QuietUnread == "" && st.MovingUnread == "" + return st +} diff --git a/modules/mesh-delivery-view/internal/view/view.go b/modules/mesh-delivery-view/internal/view/view.go index 7f8acc1..05391bd 100644 --- a/modules/mesh-delivery-view/internal/view/view.go +++ b/modules/mesh-delivery-view/internal/view/view.go @@ -1185,9 +1185,9 @@ func splitKey(key string) (node, module string) { // module's want on a node differs from its have (moving), that the have reached the want running and healthy (settled), or that it // is held or failed or stopped moving past the watchman's bound (stuck). Each is said once for a node, a module and a composition. const ( - MotionMoving = "moving" - MotionSettled = "settled" - MotionStuck = "stuck" + MotionMoving = delivery.MotionMoving + MotionSettled = delivery.MotionSettled + MotionStuck = delivery.MotionStuck // MotionBoundSeconds is how long a node may take to apply a want before the watchman says it stuck (watch.StepBound). MotionBoundSeconds = 1800 ) @@ -1316,7 +1316,6 @@ func (m *Model) motionOf(hk string) []MotionEvent { id := func(kind string) string { return kind + "." + node + "." + module + "." + strings.TrimPrefix(w.value.Composed, "sha256:") } - applied := has && h.have.FP == w.value.FP && (h.have.Composed == "" || h.have.Composed == w.value.Composed) var out []MotionEvent say := func(kind string, b MotionBody) { if m.told[id(kind)] { @@ -1325,31 +1324,25 @@ func (m *Model) motionOf(hk string) []MotionEvent { m.told[id(kind)] = true out = append(out, MotionEvent{Type: kind, ID: id(kind), Body: b}) } - switch { - case applied && h.have.Phase == delivery.PhaseRunning && h.have.Health == delivery.HealthOK: + // The derivation is mesh-sdk's (delivery.MotionOf, issue 1952 S7), shared with the condition keeper's hold. + words, stuck := m.stuckWant(w.value, node, module) + mo := delivery.MotionOf(w.value, true, h.have, has, words, stuck) + if mo.State == delivery.MotionSettled { if m.told[id(MotionMoving)] { b := body - b.Outcome = "running" + b.Outcome = mo.Outcome say(MotionSettled, b) } return out - case !m.told[id(MotionMoving)]: + } + if !m.told[id(MotionMoving)] { say(MotionMoving, body) } - stop := body - switch { - case has && h.have.Phase == delivery.PhaseHeld: - stop.Outcome, stop.Words = "stopped", h.have.Detail - case has && (h.have.Health == delivery.HealthFailed || h.have.Phase == delivery.PhaseFailed) && h.have.FP == w.value.FP: - stop.Outcome, stop.Words = "failed", h.have.Detail - default: - if s, ok := m.stuckWant(w.value, node, module); ok { - stop.Outcome, stop.Words = "stopped", s - } else { - return out - } + if mo.State == delivery.MotionStuck { + stop := body + stop.Outcome, stop.Words = mo.Outcome, mo.Words + say(MotionStuck, stop) } - say(MotionStuck, stop) return out } diff --git a/modules/mesh-delivery-view/internal/view/view_test.go b/modules/mesh-delivery-view/internal/view/view_test.go index f4f8840..9980d92 100644 --- a/modules/mesh-delivery-view/internal/view/view_test.go +++ b/modules/mesh-delivery-view/internal/view/view_test.go @@ -1,6 +1,7 @@ package view import ( + "encoding/json" "errors" "reflect" "strings" @@ -8,6 +9,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/health" ) var t0 = time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC) @@ -212,7 +214,13 @@ func stuckE(t *testing.T, bucket, key, waited string, since time.Time, moved boo return Entry{Bucket: delivery.StuckBucket, Key: bucket + "." + key, Value: b, At: since} } -func TestOfflineAndStuckAreListed(t *testing.T) { // 1168/AC2 +func TestTheWatchmansKeysAndStatus(t *testing.T) { + t.Run("offline and stuck are listed", offlineAndStuckAreListed) + t.Run("an unknown watchman version is unreadable, not dropped", unknownWatchmanVersionIsUnreadableNotDropped) + t.Run("1952/V1: status derives each part from its bucket, and says a part it cannot read", statusDerivesEachPartFromItsBucket) +} + +func offlineAndStuckAreListed(t *testing.T) { // 1168/AC2 m := New() fill(m, base(t)...) fill(m, @@ -240,7 +248,7 @@ func TestOfflineAndStuckAreListed(t *testing.T) { // 1168/AC2 } } -func TestUnknownWatchmanVersionIsUnreadableNotDropped(t *testing.T) { // 1168/AC3 +func unknownWatchmanVersionIsUnreadableNotDropped(t *testing.T) { // 1168/AC3 m := New() fill(m, Entry{Bucket: delivery.PresenceBucket, Key: "c", Value: []byte(`{"v":9,"online":false}`), At: t0}, @@ -267,3 +275,70 @@ func TestUnknownWatchmanVersionIsUnreadableNotDropped(t *testing.T) { // 1168/AC t.Fatal("a readable value must clear the unreadable mark") } } + +// 1952/V1 (ADR 0564 §10): status reads the keeper's open conditions, the watchman's presence and stuck keys, the wants against +// the haves and the healers' attempts of the week; nothing writes a summary, and a part it cannot read is said, never well. +func statusDerivesEachPartFromItsBucket(t *testing.T) { + now := t0.Add(time.Hour) + composedC := "sha256:" + strings.Repeat("c", 64) + fp1, fp2, fp3 := delivery.SourcePrefix2+strings.Repeat("1", 64), delivery.SourcePrefix2+strings.Repeat("2", 64), delivery.SourcePrefix2+strings.Repeat("3", 64) + enc := func(v any) string { b, _ := json.Marshal(v); return string(b) } + want := func(fp string) string { + b, _ := delivery.EncodeWant(delivery.Want{FP: fp, Composed: composedC}) + return string(b) + } + have := func(fp string, phase delivery.Phase, health delivery.Health, detail string) string { + b, err := delivery.EncodeHave(delivery.Have{FP: fp, Composed: composedC, Phase: phase, Health: health, Detail: detail}) + if err != nil { + t.Fatal(err) + } + return string(b) + } + presence := func(on bool) string { b, _ := delivery.EncodePresence(delivery.Presence{Online: on}); return string(b) } + stuckOf := func(bucket, key, waited string, moved bool) string { + b, _ := delivery.EncodeStuck(delivery.Stuck{Bucket: bucket, Key: key, Waited: waited, Since: t0, Moved: moved}) + return string(b) + } + stuckWant, _ := delivery.StuckKey(delivery.AssignerWantsBucket, "want.b.web") + fin := t0.Add(time.Minute) + r := StatusRead{ + Conditions: states(map[string]string{ + "machine.b.silent": enc(health.Condition{Key: "machine.b.silent", Severity: health.Warning, Summary: "b silent", Raised: t0}), + "core.doctor.self-check": enc(health.Condition{Key: "core.doctor.self-check", Severity: health.Urgent, Summary: "no run", Raised: t0.Add(time.Minute)}), + }), + Presence: states(map[string]string{"a": presence(true), "b": presence(false)}), + Stuck: states(map[string]string{stuckWant: stuckOf(delivery.AssignerWantsBucket, "want.b.web", "b to run web", false), + "x.moved": stuckOf("x", "moved", "y", true)}), + Wants: states(map[string]string{"want.a.web": want(fp2), "want.a.mail": want(fp1), "want.b.web": want(fp2), "want.a.db": want(fp3)}), + Haves: states(map[string]string{"a.web": have(fp1, delivery.PhaseRunning, delivery.HealthOK, ""), "a.mail": have(fp1, delivery.PhaseRunning, delivery.HealthOK, ""), + "b.web": have(fp1, delivery.PhaseRunning, delivery.HealthOK, ""), "a.db": have(fp3, delivery.PhaseRunning, delivery.HealthFailed, "the unit failed")}), + Attempts: states(map[string]string{"1": enc(health.Attempt{Outcome: health.OutcomeActed, Begun: t0, Finished: &fin}), + "2": enc(health.Attempt{Outcome: health.OutcomeEscalated, Begun: t0}), "old": enc(health.Attempt{Outcome: health.OutcomeActed, Begun: now.Add(-8 * 24 * time.Hour)})}), + } + st := DeriveStatus(r, now) + if len(st.Conditions) != 2 || st.Conditions[0].Key != "core.doctor.self-check" || st.Conditions[1].Key != "machine.b.silent" { + t.Errorf("conditions = %+v, want the urgent one first", st.Conditions) + } + if len(st.Quiet) != 2 || st.Quiet[0].Key != "b" || st.Quiet[1].Key != stuckWant { + t.Errorf("quiet = %+v", st.Quiet) + } + wantMoving := []StatusMoving{ + {Node: "a", Module: "db", State: delivery.MotionStuck, Outcome: delivery.OutcomeFailed, Words: "the unit failed", FromFP: fp3, ToFP: fp3}, + {Node: "a", Module: "web", State: delivery.MotionMoving, FromFP: fp1, ToFP: fp2}, + {Node: "b", Module: "web", State: delivery.MotionStuck, Outcome: delivery.OutcomeStopped, Words: "b to run web", FromFP: fp1, ToFP: fp2}, + } + if !reflect.DeepEqual(st.Moving, wantMoving) { + t.Errorf("moving = %+v\nwant %+v", st.Moving, wantMoving) + } + if !reflect.DeepEqual(st.Heals, map[string]int{health.OutcomeActed: 1, health.OutcomeEscalated: 1}) || st.Well { + t.Errorf("heals = %v, well %v", st.Heals, st.Well) + } + // A part it cannot read is said, and the mesh is not well; with nothing wrong and every part read it is. + if st := DeriveStatus(StatusRead{}, now); st.ConditionsUnread == "" || st.QuietUnread == "" || st.MovingUnread == "" || st.HealsUnread == "" || st.Well { + t.Errorf("nothing read = %+v", st) + } + empty := states(map[string]string{}) + if st := DeriveStatus(StatusRead{Conditions: empty, Presence: empty, Stuck: empty, Wants: empty, Haves: empty, Attempts: empty}, now); !st.Well { + t.Errorf("an empty mesh = %+v, want well", st) + } +} diff --git a/modules/mesh-delivery-view/manifest_test.go b/modules/mesh-delivery-view/manifest_test.go index 47727fe..70a65c1 100644 --- a/modules/mesh-delivery-view/manifest_test.go +++ b/modules/mesh-delivery-view/manifest_test.go @@ -87,7 +87,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 if m.Module != "mesh-delivery-view" { t.Errorf("module = %q", m.Module) } - want := []string{"mesh-build-go.pauses", "mesh-build-go.steps", "mesh-build-image.pauses", "mesh-build-image.steps", "mesh-build-node.pauses", "mesh-build-node.steps", "mesh-config.resolved", "mesh-controller.builds", "mesh-controller.composed", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-assigner.placement", "mesh-delivery-assigner.wants", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.heard", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.taken", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-vault.signers", "node-engine.have"} // build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9 + want := []string{"mesh-build-go.pauses", "mesh-build-go.steps", "mesh-build-image.pauses", "mesh-build-image.steps", "mesh-build-node.pauses", "mesh-build-node.steps", "mesh-conditions.conditions", "mesh-config.resolved", "mesh-controller.builds", "mesh-controller.composed", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-assigner.placement", "mesh-delivery-assigner.wants", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.heard", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.taken", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-vault.signers", "node-engine.have"} // view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9 got := slices.Clone(m.Reads) slices.Sort(got) if !reflect.DeepEqual(got, want) { @@ -119,7 +119,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 } // The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer // itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads. - if want := []string{"indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "builder_build", "builder_builds", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard"}; !reflect.DeepEqual(m.Tools, want) { + if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "builder_build", "builder_builds", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard"}; !reflect.DeepEqual(m.Tools, want) { t.Errorf("tools = %v, want %v", m.Tools, want) } for _, a := range m.Build.Artifacts { diff --git a/modules/mesh-delivery-view/module.json b/modules/mesh-delivery-view/module.json index 895fa21..5640a25 100644 --- a/modules/mesh-delivery-view/module.json +++ b/modules/mesh-delivery-view/module.json @@ -4,6 +4,7 @@ "moved": true, "slug": "view", "tools": [ + "view_status", "indexer_module", "indexer_modules", "indexer_candidate", @@ -53,6 +54,8 @@ "mesh-delivery-indexer.modules", "mesh-delivery-indexer.taken", "mesh-delivery-teller.answers", + "mesh-conditions.conditions", + "mesh-healers.attempts", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-vault.signers",