The view shows a row per module the catalogue's placement puts on a node, and reads the controller's assignments no more (issue 774, ADR 0549 §3)

This commit is contained in:
2026-10-28 21:54:47 +01:00
parent a4116a1de7
commit 9a70d697af
8 changed files with 96 additions and 132 deletions
@@ -19,7 +19,11 @@ import (
func serve(t *testing.T) string {
t.Helper()
m := view.New()
m.Apply(view.Entry{Bucket: view.AssignmentsBucket, Key: "a.x", Value: []byte(`{"node":"a","module":"x","build":"b1"}`)})
placed, err := delivery.EncodePlacement(delivery.Placement{Node: "a", Modules: []delivery.PlacedModule{{Module: "x"}}})
if err != nil {
t.Fatal(err)
}
m.Apply(view.Entry{Bucket: delivery.PlacementBucket, Key: "placement.a", Value: placed})
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "c")
if err != nil {
t.Fatal(err)
@@ -48,7 +48,7 @@ func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
kvs := map[string]jetstream.KeyValue{}
for _, b := range []string{delivery.HaveBucket, view.AssignmentsBucket, view.BuildsBucket, delivery.PresenceBucket, delivery.StuckBucket} {
for _, b := range []string{delivery.HaveBucket, delivery.PlacementBucket, view.BuildsBucket, delivery.PresenceBucket, delivery.StuckBucket} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5})
if err != nil {
t.Fatal(err)
@@ -68,9 +68,20 @@ func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8
}
return b
}
put(view.AssignmentsBucket, "a.x", []byte(`{"node":"a","module":"x","build":"b1"}`))
put(view.AssignmentsBucket, "a.y", []byte(`{"node":"a","module":"y","build":"b2"}`))
put(view.AssignmentsBucket, "b.x", []byte(`{"node":"b","module":"x","build":"b1","ended":"2026-10-18T11:30:00Z"}`))
// The catalogue's placement (issue 774, ADR 0549 §3): x and y on a, nothing on b.
placeValue := func(node string, modules ...string) []byte {
p := delivery.Placement{Node: node}
for _, m := range modules {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
}
v, err := delivery.EncodePlacement(p)
if err != nil {
t.Fatal(err)
}
return v
}
put(delivery.PlacementBucket, "placement.a", placeValue("a", "x", "y"))
put(delivery.PlacementBucket, "placement.b", placeValue("b"))
put(view.BuildsBucket, "x.build.b1", []byte(`{"id":"b1","module":"x"}`))
put(view.BuildsBucket, "x.newest", []byte("b1"))
put(delivery.HaveBucket, "a.x", haveValue("b1", delivery.PhaseChecking, delivery.HealthWaits))
@@ -150,7 +161,7 @@ func TestProcessWholeShowsWhatTheWatchmanSays(t *testing.T) { // 1168/AC5
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
kvs := map[string]jetstream.KeyValue{}
for _, b := range []string{delivery.HaveBucket, view.AssignmentsBucket, view.BuildsBucket, delivery.PresenceBucket, delivery.StuckBucket} {
for _, b := range []string{delivery.HaveBucket, delivery.PlacementBucket, view.BuildsBucket, delivery.PresenceBucket, delivery.StuckBucket} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5})
if err != nil {
t.Fatal(err)
@@ -236,7 +247,7 @@ func TestProcessWholeShowsTheIndexersBucketsOnceTheyExist(t *testing.T) { // 124
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
for _, b := range []string{delivery.HaveBucket, view.AssignmentsBucket, view.BuildsBucket, delivery.PresenceBucket, delivery.StuckBucket} {
for _, b := range []string{delivery.HaveBucket, delivery.PlacementBucket, view.BuildsBucket, delivery.PresenceBucket, delivery.StuckBucket} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5}); err != nil {
t.Fatal(err)
}
@@ -1,4 +1,4 @@
// Command mesh-delivery-view is the operator's view of delivery (novox/hq ADR 0429 §3): it watches the have, assignments
// Command mesh-delivery-view is the operator's view of delivery (novox/hq ADR 0429 §3): it watches the have, the catalogue's placement
// and builds buckets on its own bus credential, derives one row per module and node in memory, and serves them on a
// local socket (for mesh-delivery-cli) and on a local web page. It starts, stops or retries nothing; the one thing it writes is the operator's answer to an ask that needs no proof of who
// answered, under `<ask key>.view` in its bucket answers (ADR 0528 clause 6).
@@ -106,7 +106,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return err
})
model.SetBacklog(view.BacklogOver(js, 5*time.Second))
for _, bucket := range []string{delivery.HaveBucket, view.AssignmentsBucket, view.BuildsBucket, delivery.PresenceBucket, delivery.StuckBucket, delivery.CandidatesBucket, delivery.ModulesBucket, delivery.AsksBucket, delivery.AnswersBucket, delivery.BuilderBuildsBucket, delivery.AssignerWantsBucket, delivery.SourceBucket, delivery.ControllerComposedBucket, delivery.AssignerAsksBucket, delivery.ViewAnswersBucket, delivery.TakenBucket} {
for _, bucket := range []string{delivery.HaveBucket, delivery.PlacementBucket, view.BuildsBucket, delivery.PresenceBucket, delivery.StuckBucket, delivery.CandidatesBucket, delivery.ModulesBucket, delivery.AsksBucket, delivery.AnswersBucket, delivery.BuilderBuildsBucket, delivery.AssignerWantsBucket, delivery.SourceBucket, delivery.ControllerComposedBucket, delivery.AssignerAsksBucket, delivery.ViewAnswersBucket, delivery.TakenBucket} {
go follow(ctx, js, bucket, false, model, logger)
}
// The modules' and the source's history give each fingerprint's first commit and each head's source revision (ADR 0556).
@@ -4,6 +4,7 @@ package view
import (
"encoding/json"
"fmt"
"maps"
"slices"
"sort"
"strings"
@@ -215,7 +216,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
assigns map[string]assignment
placed *delivery.Placed // the catalogue's placement (the indexer's state, ADR 0549 §3)
builds map[string]buildEntry // by <module>.build.<id>
groups map[string]groupEntry // the indexer's candidates, by group
modules map[string]moduleEntry // the indexer's modules, by module
@@ -263,22 +264,20 @@ type stuckEntry struct {
err string
}
// assignment holds only the fields of the controller's Assignment the view reads.
type assignment struct {
Node string `json:"node"`
Module string `json:"module"`
Build string `json:"build"`
Commit string `json:"commit"`
Since time.Time `json:"since"`
Ended time.Time `json:"ended"`
State string `json:"state"`
PulledIn bool `json:"pulled-in"`
RequiredBy string `json:"required-by"`
}
// pair is a module the catalogue's placement puts on a node.
type pair struct{ Node, Module string }
// open says whether the value is an assignment that stands for the module on its node, as the controller's
// Assignment.Stands does. A module pulled in by another stands too.
func (a assignment) open() bool { return a.Ended.IsZero() && a.State == "" }
// placedPairs is every module the catalogue's placement puts on a node, by node then module. The caller holds mu.
func (m *Model) placedPairs() []pair {
byNode := m.placed.ByNode()
var out []pair
for _, node := range slices.Sorted(maps.Keys(byNode)) {
for _, module := range byNode[node] {
out = append(out, pair{Node: node, Module: module})
}
}
return out
}
// build holds only the fields of the controller's Build record the view reads. A record with no state and no failure
// is a done build.
@@ -356,7 +355,7 @@ type nodeEntry struct {
// New is an empty model.
func New() *Model {
return &Model{assigns: map[string]assignment{}, builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, asks: map[string]askRecord{}, wants: map[string]wantEntry{}, told: map[string]bool{}, initial: map[string]bool{}, composed: map[string]composedEntry{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]delivery.ViewAnswer{}, answers: map[string]string{}, answerOf: map[string]delivery.Answer{}, holds: map[string]heldEntry{}, newest: map[string]string{},
return &Model{placed: delivery.NewPlaced(), builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, asks: map[string]askRecord{}, wants: map[string]wantEntry{}, told: map[string]bool{}, initial: map[string]bool{}, composed: map[string]composedEntry{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]delivery.ViewAnswer{}, answers: map[string]string{}, answerOf: map[string]delivery.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][]delivery.Module{}, srcHist: map[uint64]map[string]string{}, taken: map[string]delivery.Taken{},
@@ -529,16 +528,9 @@ func (m *Model) apply(e Entry) {
}
st, err := delivery.DecodeStuck(e.Value)
m.stuck[e.Key] = stuckEntry{value: st, err: errText(err)}
case AssignmentsBucket:
if e.Deleted {
delete(m.assigns, e.Key)
return
}
var a assignment
if json.Unmarshal(e.Value, &a) != nil || a.Node == "" || a.Module == "" {
return
}
m.assigns[a.Node+"."+a.Module] = a
case delivery.PlacementBucket:
// A key that does not read places nothing on its node; the indexer's own read tool says why.
_, _, _ = m.placed.Put(e.Key, e.Value, e.Deleted, e.At)
case delivery.CandidatesBucket:
if e.Deleted {
delete(m.groups, e.Key)
@@ -714,18 +706,12 @@ func (m *Model) snapshot() Snapshot {
m.mu.Lock()
defer m.mu.Unlock()
rows := []Row{}
for key, a := range m.assigns {
if !a.open() {
continue
}
r := Row{Node: a.Node, Module: a.Module, Status: StatusAssigned, Assigned: a.Build}
if a.PulledIn {
r.RequiredBy = a.RequiredBy
}
for _, a := range m.placedPairs() {
r := Row{Node: a.Node, Module: a.Module, Status: StatusAssigned}
var waits []string
hk, herr := delivery.HaveKey(a.Node, a.Module)
if herr != nil {
hk = key
continue
}
h, has := m.haves[hk]
// The controller no longer writes the build of an assignment (R6): the row is the want and the have of the node.
@@ -745,17 +731,8 @@ func (m *Model) snapshot() Snapshot {
r.Have = h.have.FP
}
r.Since, r.Detail = h.at, h.have.Detail
if wanted {
if h.have.FP != w.value.FP {
waits = append(waits, "the node has not applied "+w.value.FP)
}
} else if a.Build != "" && h.have.Build != a.Build {
waits = append(waits, "the node has not applied "+a.Build)
}
}
if id := m.newest[a.Module]; id != "" && a.Build != "" && id != a.Build {
if b, ok := m.builds[a.Module+".build."+id]; ok && b.done() {
waits = append(waits, a.Module+" has a newer build "+id+" not yet assigned")
if wanted && h.have.FP != w.value.FP {
waits = append(waits, "the node has not applied "+w.value.FP)
}
}
r.Waits = strings.Join(waits, "; ")
@@ -766,7 +743,7 @@ func (m *Model) snapshot() Snapshot {
if perr != nil {
continue
}
if a, ok := m.assigns[node+"."+module]; ok && a.open() {
if _, ok := m.placed.Since(node, module); ok {
continue
}
r := Row{Node: node, Module: module, Status: StatusLeftBehind, Since: h.at, Error: h.err}
@@ -984,8 +961,7 @@ func (s Snapshot) Counted() int {
// The buckets the view reads besides the have bucket (which mesh-sdk's delivery package names).
const (
AssignmentsBucket = "mesh-controller_assignments"
BuildsBucket = "mesh-controller_builds"
BuildsBucket = "mesh-controller_builds"
)
// The statuses of a row.
@@ -1176,13 +1152,10 @@ func (m *Model) sourceRow() *SourceRow {
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s, not yet running and healthy", module, node, short(w.value.FP)))
}
}
for key, a := range m.assigns {
if !a.open() {
continue
}
for _, a := range m.placedPairs() {
wk, werr := delivery.HaveKey(a.Node, a.Module)
if werr != nil {
wk = key
continue
}
if _, ok := m.wants[wk]; ok {
continue
@@ -25,9 +25,22 @@ func haveE(t *testing.T, node, module string, v []byte, at time.Time) Entry {
return Entry{Bucket: delivery.HaveBucket, Key: node + "." + module, Value: v, At: at}
}
func assign(node, module, build, extra string) Entry {
v := `{"node":"` + node + `","module":"` + module + `","build":"` + build + `","commit":"c","since":"2026-10-18T11:00:00Z"` + extra + `}`
return Entry{Bucket: AssignmentsBucket, Key: node + "." + module, Value: []byte(v), At: t0}
// place is the catalogue's placement of node, as the indexer keeps it (ADR 0549 §2): the modules placed there.
func place(t *testing.T, node string, modules ...string) Entry {
t.Helper()
p := delivery.Placement{Node: node}
for _, m := range modules {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
}
key, err := delivery.PlacementKey(node)
if err != nil {
t.Fatal(err)
}
v, err := delivery.EncodePlacement(p)
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.PlacementBucket, Key: key, Value: v, At: t0}
}
func buildRec(module, id, extra string) []Entry {
@@ -58,7 +71,7 @@ func fill(m *Model, es ...Entry) {
// base: two nodes (a, b), three modules (x, y, z).
func base(t *testing.T) []Entry {
es := []Entry{
assign("a", "x", "b1", ""), assign("a", "y", "b2", ""), assign("b", "x", "b1", ""), assign("b", "z", "b3", ""),
place(t, "a", "x", "y"), place(t, "b", "x", "z"),
haveE(t, "a", "x", have(t, "b1", delivery.PhaseRunning, delivery.HealthOK, ""), t0.Add(1*time.Minute)),
haveE(t, "b", "x", have(t, "b1", delivery.PhaseChecking, delivery.HealthWaits, "starting"), t0.Add(2*time.Minute)),
haveE(t, "b", "z", have(t, "b3", delivery.PhaseRunning, delivery.HealthOK, ""), t0.Add(3*time.Minute)),
@@ -77,68 +90,35 @@ func TestRowsPerAssignedModuleAndNode(t *testing.T) { // 1134/AC1
t.Fatalf("want 4 rows, got %d counted %d: %+v", len(s.Rows), s.Counted(), s.Rows)
}
r := find(t, s, "b", "x")
want := Row{Node: "b", Module: "x", Status: StatusAssigned, Assigned: "b1", Have: "b1", Phase: "checking", Health: "waits",
want := Row{Node: "b", Module: "x", Status: StatusAssigned, Have: "b1", Phase: "checking", Health: "waits",
Since: t0.Add(2 * time.Minute), Detail: "starting"}
if !reflect.DeepEqual(r, want) {
t.Fatalf("row = %+v, want %+v", r, want)
}
y := find(t, s, "a", "y")
if y.Waits != "no have yet" || y.Assigned != "b2" || y.Have != "" {
if y.Waits != "no have yet" || y.Have != "" {
t.Fatalf("a.y = %+v, want 'no have yet'", y)
}
}
func TestWaitsForTheNodeAndForANewerBuild(t *testing.T) { // 1134/AC2
func TestLeftBehindIsWhatThePlacementDoesNotPut(t *testing.T) { // 1134/AC3, issue 774 (the catalogue's placement)
m := New()
fill(m, base(t)...)
fill(m, haveE(t, "a", "x", have(t, "b0", delivery.PhaseRunning, delivery.HealthOK, ""), t0))
if w := find(t, m.Snapshot(), "a", "x").Waits; w != "the node has not applied b1" {
t.Fatalf("waits = %q", w)
}
fill(m, buildRec("z", "b4", "")...)
if w := find(t, m.Snapshot(), "b", "z").Waits; w != "z has a newer build b4 not yet assigned" {
t.Fatalf("waits = %q", w)
}
// a newest build that is not done is not a newer build to assign
fill(m, buildRec("z", "b5", `,"failed":"boom"`)...)
if w := find(t, m.Snapshot(), "b", "z").Waits; w != "" {
t.Fatalf("a failed newest build must not be waited on, waits = %q", w)
}
fill(m, buildRec("z", "b6", `,"state":"asked"`)...)
if w := find(t, m.Snapshot(), "b", "z").Waits; w != "" {
t.Fatalf("an asked newest build must not be waited on, waits = %q", w)
}
}
func TestLeftBehindIsNotRunningAndPulledInIs(t *testing.T) { // 1134/AC3 (both halves)
for name, extra := range map[string]string{"ended": `,"ended":"2026-10-18T11:30:00Z"`, "state": `,"state":"pending"`} {
m := New()
fill(m, base(t)...)
fill(m, assign("b", "z", "b3", extra))
s := m.Snapshot()
r := find(t, s, "b", "z")
if r.Status != StatusLeftBehind {
t.Errorf("%s: status = %q, want %q", name, r.Status, StatusLeftBehind)
}
if s.Counted() != 3 {
t.Errorf("%s: counted = %d, want 3", name, s.Counted())
}
}
m := New()
fill(m, base(t)...)
fill(m, haveE(t, "a", "w", have(t, "b9", delivery.PhaseRunning, delivery.HealthOK, ""), t0)) // no assignment at all
fill(m, place(t, "b", "x")) // z taken off b
s := m.Snapshot()
if find(t, s, "a", "w").Status != StatusLeftBehind || s.Counted() != 4 {
t.Fatalf("a have with no assignment: %+v counted %d", find(t, s, "a", "w"), s.Counted())
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)
}
// A module pulled in by another stands: shown and counted, with what required it.
m = New()
fill(m, base(t)...)
fill(m, assign("b", "z", "b3", `,"pulled-in":true,"required-by":"x"`))
fill(m, haveE(t, "a", "w", have(t, "b9", delivery.PhaseRunning, delivery.HealthOK, ""), t0)) // placed nowhere
s = m.Snapshot()
r := find(t, s, "b", "z")
if r.Status != StatusAssigned || r.RequiredBy != "x" || r.Have != "b3" || s.Counted() != 4 {
t.Fatalf("pulled-in row = %+v counted %d, want shown and counted with required-by x", r, s.Counted())
if find(t, s, "a", "w").Status != StatusLeftBehind || s.Counted() != 4 {
t.Fatalf("a have with no placement: %+v counted %d", find(t, s, "a", "w"), s.Counted())
}
fill(m, Entry{Bucket: delivery.PlacementBucket, Key: "placement.b", Deleted: true})
if s := m.Snapshot(); s.Counted() != 2 || find(t, s, "b", "x").Status != StatusLeftBehind {
t.Fatalf("b's placement deleted: counted %d, %+v", s.Counted(), s.Rows)
}
}
@@ -181,18 +161,13 @@ func TestOnlyTheAffectedRowChanges(t *testing.T) { // 1134/AC5
if r := find(t, after, "b", "x"); r.Phase != "running" || r.Health != "ok" || !r.Since.Equal(t0.Add(9*time.Minute)) {
t.Fatalf("changed row = %+v", r)
}
// an assignment change and a build change touch only their rows
fill(m, assign("a", "y", "b7", ""))
if r := find(t, m.Snapshot(), "a", "y"); r.Assigned != "b7" {
t.Fatalf("assignment change not seen: %+v", r)
// a want's change touches only its row
fill(m, wantE(t, "a", "y", delivery.Want{FP: fpNew}))
if r := find(t, m.Snapshot(), "a", "y"); r.Assigned != fpNew {
t.Fatalf("want change not seen: %+v", r)
}
if !reflect.DeepEqual(find(t, m.Snapshot(), "b", "z"), find(t, after, "b", "z")) {
t.Fatal("an assignment of another row changed b.z")
}
fill(m, buildRec("x", "b8", "")...)
s := m.Snapshot()
if find(t, s, "a", "x").Waits == "" || find(t, s, "b", "x").Waits == "" || find(t, s, "b", "z").Waits != "" {
t.Fatalf("a build of x must touch x's rows only: %+v", s.Rows)
t.Fatal("a want of another row changed b.z")
}
// a deleted key (a bucket's expiry) removes its value
m.Apply(Entry{Bucket: delivery.HaveBucket, Key: "a.x", Deleted: true})
@@ -101,7 +101,7 @@ func TestEveryWantIsARowAgainstItsHave(t *testing.T) {
func TestASourceRevisionIsDerivedFromWantsAndHaves(t *testing.T) {
m := New()
fill(m, sourceE(t, 14355), moduleE(t, "web", fpNew), moduleE(t, "mail", fpNew),
assign("ace", "web", "b-1", ""), assign("g14", "web", "b-1", ""),
place(t, "ace", "web"), place(t, "g14", "web"),
wantE(t, "ace", "web", delivery.Want{FP: fpNew}), haveFP(t, "ace", "web", fpNew, delivery.HealthOK),
wantE(t, "g14", "web", delivery.Want{FP: fpNew}), haveFP(t, "g14", "web", fpNew, delivery.HealthOK),
wantE(t, "ace", "mail", delivery.Want{FP: fpNew}), haveFP(t, "ace", "mail", fpNew, delivery.HealthOK))
@@ -119,7 +119,7 @@ func TestASourceRevisionIsDerivedFromWantsAndHaves(t *testing.T) {
if s.State != SourceNotDelivered || !strings.Contains(strings.Join(s.Waiting, ";"), "mail") {
t.Errorf("a want that is not the module's fingerprint: %+v", s)
}
fill(m, wantE(t, "ace", "mail", delivery.Want{FP: fpNew}), assign("novox", "web", "b-1", ""))
fill(m, wantE(t, "ace", "mail", delivery.Want{FP: fpNew}), place(t, "novox", "web"))
s = m.Snapshot().Source
if s.State != SourceNotDelivered || !strings.Contains(strings.Join(s.Waiting, ";"), "novox") {
t.Errorf("a node assigned the module with no want: %+v", s)
@@ -177,7 +177,7 @@ func TestTheViewSaysOnceThatASourceRevisionIsDelivered(t *testing.T) {
said = append(said, e)
return nil
})
fill(m, sourceE(t, 14355), moduleE(t, "web", fpNew), assign("ace", "web", "b-1", ""),
fill(m, sourceE(t, 14355), moduleE(t, "web", fpNew), place(t, "ace", "web"),
wantE(t, "ace", "web", delivery.Want{FP: fpNew}))
if len(said) != 0 {
t.Fatalf("said %v before any have", said)
@@ -213,7 +213,7 @@ func TestTheViewSaysOnceThatASourceRevisionIsDelivered(t *testing.T) {
// have: the row shows the fingerprint wanted and waits while the node runs another.
func TestARowReadsTheWantWhenTheAssignmentNamesNoBuild(t *testing.T) {
m := New()
fill(m, assign("ace", "web", "", ""), wantE(t, "ace", "web", delivery.Want{FP: fpNew}), haveFP(t, "ace", "web", fpOld, delivery.HealthOK))
fill(m, place(t, "ace", "web"), wantE(t, "ace", "web", delivery.Want{FP: fpNew}), haveFP(t, "ace", "web", fpOld, delivery.HealthOK))
r := find(t, m.Snapshot(), "ace", "web")
if r.Assigned != fpNew || r.Have != fpOld || !strings.Contains(r.Waits, "has not applied") {
t.Fatalf("row = %+v, want it to wait for the want's fingerprint", r)
@@ -228,10 +228,12 @@ func TestARowReadsTheWantWhenTheAssignmentNamesNoBuild(t *testing.T) {
// and a have nothing is assigned to keeps the module's name.
func TestADottedModuleNameIsFoundAndKept(t *testing.T) {
m := New()
fill(m, assign("ace", "novox.be", "", ""), wantE(t, "ace", "novox.be", delivery.Want{FP: fpNew}), haveFP(t, "ace", "novox.be", fpNew, delivery.HealthOK),
// The catalogue's placement refuses a dotted module name (mesh-sdk delivery.Placement), 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", delivery.Want{FP: fpNew}), haveFP(t, "ace", "novox.be", fpNew, delivery.HealthOK),
haveFP(t, "ace", "old.name", fpNew, delivery.HealthOK))
s := m.Snapshot()
if r := find(t, s, "ace", "novox.be"); r.Have != fpNew || r.Waits != "" {
if r := find(t, s, "ace", "novox.be"); r.Have != fpNew || r.Status != StatusLeftBehind {
t.Errorf("the dotted module's row = %+v", r)
}
if r := find(t, s, "ace", "old.name"); r.Status != StatusLeftBehind {
+1 -1
View File
@@ -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.assignments", "mesh-controller.builds", "mesh-controller.composed", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "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", "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-config.resolved", "mesh-controller.builds", "mesh-controller.composed", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "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", "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
got := slices.Clone(m.Reads)
slices.Sort(got)
if !reflect.DeepEqual(got, want) {
-1
View File
@@ -30,7 +30,6 @@
{ "name": "reach", "history": 1 }
],
"reads": [
"mesh-controller.assignments",
"mesh-controller.builds",
"mesh-build-go.pauses",
"mesh-build-go.steps",