Compare commits

...
2 Commits
Author SHA1 Message Date
jschoubben 289d1e8fa6 Format mesh-delivery-view's read.go as gofmt writes it (issue 1126, the group check's failure)
mesh/delivery ready: it is merged on the Approve
mesh/check pass 289d1e8fa69a on bab2e1391ae5 | unit 2860 run, 1141 kept, 36 skipped | integration 2887 run, 1241 kept, 29 skipped | over bound: unit (…
mesh/delivery-group group fix/1126-shadow-bodies ready: every member ready, and composed together they pass
2026-10-28 01:49:54 +01:00
jschoubben 968b9bf2e6 mesh-delivery-view: a node's candidate answer is shown as the node wrote it; pin mesh-sdk 362a3f1 (issue 1126, probe #532)
mesh/check failed (2 tests): unit: modules/mesh-delivery-view.gofmt: not as gofmt writes them (run gofmt -w): modules/mesh-delivery-view/internal/view…
mesh/delivery-group group fix/1126-shadow-bodies rejected: composed together: the checker's verdict at the members' heads (46e1e846f8db): unit: modules/mesh-de…
mesh/delivery closed: a newer head of the same pull request
The bodies of each difference never reached the answer shown: the view decoded
the node's answer with its mesh-sdk, which did not know the field, and encoded
it again. The answer now rides through as stored; a decoded copy only says its
state.
2026-10-28 01:39:34 +01:00
4 changed files with 50 additions and 21 deletions
@@ -154,7 +154,10 @@ func TestModulesToolAnswersEveryKeyAsStored(t *testing.T) {
return raw, revs[key], nil
}}
p.HavesState = fake{
"ace.node": `{"v":1,"composed":"x","candidates":{"feat/p":{"head":"` + h2 + `","verdict":"same","same":["plex"]}}}`,
// ace's answer as the node wrote it, with what this view's mesh-sdk may not know (issue 1126, probe #532: the
// bodies of each difference were dropped on the way): shown as stored.
"ace.node": `{"v":1,"composed":"x","candidates":{"feat/p":{"head":"` + h2 + `","verdict":"same","same":["plex"],` +
`"differs":[],"note":"took over a minute","future":{"bodies":[{"id":"x","composed":"<masked>"}]}}}}`,
"g14.node": `{"v":1,"composed":"x","candidates":{"feat/p":{"head":"` + h1 + `","revision":2,"verdict":"same"}}}`,
}.reader().State
out, err = run(t, p, view.PlacementReadTool, map[string]any{})
@@ -164,6 +167,9 @@ func TestModulesToolAnswersEveryKeyAsStored(t *testing.T) {
answered := map[string]string{}
for _, c := range out.(map[string]any)["candidates"].([]view.CandidateRow) {
answered[c.Node] = c.Answered
if c.Node == "ace" && (!strings.Contains(string(c.Answer), `"future":{"bodies"`) || !strings.Contains(string(c.Answer), `"note":"took over a minute"`)) {
t.Errorf("ace's answer is not shown as stored: %s", c.Answer)
}
}
if want := map[string]string{"ace": "same", "g14": view.AnswerStale, "novox": view.AnswerNone}; fmt.Sprint(answered) != fmt.Sprint(want) {
t.Errorf("answers %v, want %v", answered, want)
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-view
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027193820-8ad9372b1952
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028003907-362a3f1378aa
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2
View File
@@ -1,5 +1,7 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027193820-8ad9372b1952 h1:TmveT3Bija643RGWkilvkzP/h8UWlVvHFZZtQfJK5oU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027193820-8ad9372b1952/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028003907-362a3f1378aa h1:DFYX7pIluzeeEyM+YXGeB6GvEnOYyRhz/1Ke/LFa8+8=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028003907-362a3f1378aa/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=
@@ -726,14 +726,16 @@ const (
// CandidateRow is one placement candidate and the node's answer to it.
type CandidateRow struct {
Group string `json:"group"`
Node string `json:"node"`
Pull delivery.Head `json:"pull"`
Revision uint64 `json:"revision"`
Placement json.RawMessage `json:"placement,omitempty"` // the candidate value as stored
Removes bool `json:"removes,omitempty"` // the head removes the node's file
Answered string `json:"answered"` // same, differs, refused, stale or none
Answer *delivery.CandidateAnswer `json:"answer,omitempty"`
Group string `json:"group"`
Node string `json:"node"`
Pull delivery.Head `json:"pull"`
Revision uint64 `json:"revision"`
Placement json.RawMessage `json:"placement,omitempty"` // the candidate value as stored
Removes bool `json:"removes,omitempty"` // the head removes the node's file
Answered string `json:"answered"` // same, differs, refused, stale or none
// Answer is the node's answer as its node wrote it, whole: what this view's mesh-sdk does not know rides through (issue
// 1126, probe #532: each difference's bodies were dropped when the answer was decoded and encoded again here).
Answer json.RawMessage `json:"answer,omitempty"`
}
// PlacementCandidates is every placement candidate, narrowed by node and by a module its placement names, with each
@@ -773,8 +775,8 @@ func (r Reader) PlacementCandidates(node, module string) ([]CandidateRow, error)
placed map[string]bool
}
groups := map[string]*group{}
answers := map[string]map[string]delivery.CandidateAnswer{} // node, then group
answerOf := func(n, g string) (delivery.CandidateAnswer, bool) {
answers := map[string]map[string]storedAnswer{} // node, then group
answerOf := func(n, g string) (storedAnswer, bool) {
if _, read := answers[n]; !read {
answers[n] = r.candidateAnswers(n)
}
@@ -818,8 +820,8 @@ func (r Reader) PlacementCandidates(node, module string) ([]CandidateRow, error)
continue
}
if a, ok := answerOf(n, gname); ok && err == nil {
row.Answer = &a
row.Answered = AnswerOf(c.Pull.Head, rev, true, a)
row.Answer = a.raw
row.Answered = AnswerOf(c.Pull.Head, rev, true, a.a)
}
rows = append(rows, row)
}
@@ -841,8 +843,8 @@ func (r Reader) PlacementCandidates(node, module string) ([]CandidateRow, error)
}
row := CandidateRow{Group: gname, Node: n, Pull: g.pull, Answered: AnswerNone}
if a, ok := answerOf(n, gname); ok {
row.Answer = &a
row.Answered = AnswerOf(g.pull.Head, 0, false, a)
row.Answer = a.raw
row.Answered = AnswerOf(g.pull.Head, 0, false, a.a)
}
rows = append(rows, row)
}
@@ -859,8 +861,15 @@ func AnswerOf(head string, rev uint64, keyed bool, a delivery.CandidateAnswer) s
return a.Verdict
}
// candidateAnswers is a node's answers to its candidates, by group, from its `<node>.node` have; none when it has none.
func (r Reader) candidateAnswers(node string) map[string]delivery.CandidateAnswer {
// storedAnswer is a node's answer to one group: as stored, and read for its head, revision and verdict.
type storedAnswer struct {
raw json.RawMessage
a delivery.CandidateAnswer
}
// candidateAnswers is a node's answers to its candidates, by group, from its `<node>.node` have, each as stored; none
// when it has none.
func (r Reader) candidateAnswers(node string) map[string]storedAnswer {
if r.HavesState.Get == nil {
return nil
}
@@ -872,9 +881,21 @@ func (r Reader) candidateAnswers(node string) map[string]delivery.CandidateAnswe
if err != nil || len(raw) == 0 {
return nil
}
h, err := delivery.DecodeHaveNode(raw)
if err != nil {
if _, err := delivery.DecodeHaveNode(raw); err != nil {
return nil
}
return h.Candidates
var stored struct {
Candidates map[string]json.RawMessage `json:"candidates"`
}
if json.Unmarshal(raw, &stored) != nil {
return nil
}
out := make(map[string]storedAnswer, len(stored.Candidates))
for g, ans := range stored.Candidates {
var a delivery.CandidateAnswer
if json.Unmarshal(ans, &a) == nil {
out[g] = storedAnswer{raw: ans, a: a}
}
}
return out
}