From 968b9bf2e6b1858a34ae9cf7cc3074d790fbb61d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 01:39:34 +0100 Subject: [PATCH 1/2] mesh-delivery-view: a node's candidate answer is shown as the node wrote it; pin mesh-sdk 362a3f1 (issue 1126, probe #532) 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. --- .../cmd/mesh-delivery-view-tools/main_test.go | 8 +++- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 2 + .../mesh-delivery-view/internal/view/read.go | 45 ++++++++++++++----- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go index 3e91541..7fae9a3 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go @@ -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":""}]}}}}`, "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) diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index a3bb0c8..8efad1b 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.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 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index fa4f833..2513437 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -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= diff --git a/modules/mesh-delivery-view/internal/view/read.go b/modules/mesh-delivery-view/internal/view/read.go index d7e3ae3..df98c6a 100644 --- a/modules/mesh-delivery-view/internal/view/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -733,7 +733,9 @@ type CandidateRow struct { 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"` + // 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` 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` 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 } From 289d1e8fa69ac28e244cf8f9a3439d044101d829 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 01:49:54 +0100 Subject: [PATCH 2/2] Format mesh-delivery-view's read.go as gofmt writes it (issue 1126, the group check's failure) --- modules/mesh-delivery-view/internal/view/read.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/mesh-delivery-view/internal/view/read.go b/modules/mesh-delivery-view/internal/view/read.go index df98c6a..8f32bdf 100644 --- a/modules/mesh-delivery-view/internal/view/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -726,13 +726,13 @@ 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 + 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"`