From 82d0e125b2c0638861e7b47f5f990ff2f529d8dd Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 01:42:17 +0100 Subject: [PATCH] What runs is the composition the have names, never the copy of the declaration kept before the switch (issue 1126, probe #532: shanks' xterm, zsh and its plugins answered differing at the very digest their haves say runs) --- internal/selfcompose/selfcompose_test.go | 22 ++++++++++ internal/selfcompose/shadow.go | 53 +++++++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 4df0ee4..9b6e264 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -751,6 +751,28 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { t.Fatal("the shadow, once ready, answered nothing") } + // Issue 1126, probe #532 on shanks: what runs is the composition the have names, never the copy of the declaration + // kept before the switch (no fp, no digest): a module composed at the digest its have says runs is the same. + { + probe := delivery.CandidateAnswer{Head: "h", Revision: 68, Verdict: delivery.CandidateDiffers, Same: []string{"adwaita"}, + Differs: []delivery.ModuleDiffers{ + {Module: "xterm", Composed: "sha256:3c0a31df64283c0678b1cdbeff8ac030609cab65cc7e47faaf714da743d9340e", Why: "the fingerprint differs"}, + {Module: "zsh", Composed: "sha256:6093f31056d39eaf604410b7289ec6b5fe602b19c8547283562fcfc078c4844f", Resource: "zsh.bundle-tools", Why: "the resource differs"}, + {Module: "brother", Composed: "sha256:0702b13991288da72d230460624e949d3c1d7d833eac2ec2134073c82e5a61cf", + Running: "sha256:c11c14af2738e3fcdb04da4004bbe84728bdefaf8c986d76601ffbe502435871", Resource: "brother.needs-broker-device", Why: "the resource differs"}}} + got := asHavesSay(probe, map[string]string{ + "xterm": "sha256:3c0a31df64283c0678b1cdbeff8ac030609cab65cc7e47faaf714da743d9340e", + "zsh": "sha256:6093f31056d39eaf604410b7289ec6b5fe602b19c8547283562fcfc078c4844f", + "brother": "sha256:c11c14af2738e3fcdb04da4004bbe84728bdefaf8c986d76601ffbe502435871"}) + if fmt.Sprint(got.Same) != "[adwaita xterm zsh]" || len(got.Differs) != 1 || got.Differs[0].Module != "brother" || got.Verdict != delivery.CandidateDiffers { + t.Errorf("shanks' probe answered %+v", got) + } + if one := asHavesSay(delivery.CandidateAnswer{Head: "h", Verdict: delivery.CandidateDiffers, Differs: probe.Differs[:1]}, + map[string]string{"xterm": probe.Differs[0].Composed}); one.Verdict != delivery.CandidateSame { + t.Errorf("a module the same as its have says is answered %+v", one) + } + } + // The way back, row by row (review of the shadow, round 1): the controller's compositions at n1's wants compared // with what runs; one differing is answered so; one the controller holds none of refuses, without returning. var runsList []delivery.Composed diff --git a/internal/selfcompose/shadow.go b/internal/selfcompose/shadow.go index 43818e3..f5a3b9a 100644 --- a/internal/selfcompose/shadow.go +++ b/internal/selfcompose/shadow.go @@ -36,6 +36,9 @@ type Shadow struct { // wants are the node's wants by module (agent.NodeModule for its own): what a head deleting its placement returns // it to, the controller's compositions at them. wants map[string]delivery.Want + // ran is, by module, the digest the module's have says runs: what runs, where the agent's list holds only the copy + // from the declaration kept before the switch (issue 1126, probe #532 on shanks). + ran map[string]string } // candidateKey is one candidate key as last read: its value and revision. @@ -47,7 +50,7 @@ type candidateKey struct { // NewShadow is the shadow composer of one node; answer writes the node's answers into its own have key. func NewShadow(env Env, answer func(ctx context.Context, answers map[string]delivery.CandidateAnswer)) *Shadow { return &Shadow{c: New(env), answer: answer, candidates: map[string]map[string]candidateKey{}, - answers: map[string]delivery.CandidateAnswer{}, wants: map[string]delivery.Want{}} + answers: map[string]delivery.CandidateAnswer{}, wants: map[string]delivery.Want{}, ran: map[string]string{}} } // Keep runs the shadow until ctx ends, beginning again after backoff(try) whenever it stops: an input it cannot watch @@ -90,6 +93,7 @@ func (s *Shadow) Run(ctx context.Context) error { s.mu.Lock() s.candidates = map[string]map[string]candidateKey{} s.wants = map[string]delivery.Want{} + s.ran = map[string]string{} s.c = New(s.c.env) s.mu.Unlock() c := s.c @@ -158,6 +162,7 @@ func (s *Shadow) Run(ctx context.Context) error { dirty = s.takeRecord(t.u) || dirty case t.in == 5: c.takeModuleHave(t.u) + s.takeRan(t.u) dirty = !isNodeKey(t.u.Key) || dirty case t.in == 6: dirty = s.takeWant(t.u) || dirty @@ -398,7 +403,51 @@ func (s *Shadow) answerGroup(ctx context.Context, group string) (delivery.Candid c.mu.Unlock() a := frombus.Answer(ctx, reader{c: c}, c.env.Mesh, frombus.Scope{Node: node, KeptAt: kept, Placements: given, InOwn: inOwn, SameValue: c.sameValue}, fps, running, head, rev) - return a, true + s.mu.Lock() + ran := make(map[string]string, len(s.ran)) + for m, d := range s.ran { + ran[m] = d + } + s.mu.Unlock() + return asHavesSay(a, ran), true +} + +// takeRan keeps the digest one of this node's module haves says runs. +func (s *Shadow) takeRan(u agent.Update) { + node, module, isNode, err := delivery.ParseHaveKey(u.Key) + if err != nil || isNode || node != s.c.env.Node { + return + } + s.mu.Lock() + defer s.mu.Unlock() + h, err := delivery.DecodeHave(u.Value) + if u.Deleted || err != nil || h.Composed == "" || h.Phase == delivery.PhaseRemoved { + delete(s.ran, module) + return + } + s.ran[module] = h.Composed +} + +// asHavesSay is a with each module that differs only from the copy the agent keeps of the declaration before the switch +// counted the same where the composed digest is the one its have says runs: what runs is the composition at that +// digest, never the kept copy (issue 1126, probe #532: shanks' xterm, zsh, zsh-autosuggestions and +// zsh-syntax-highlighting). +func asHavesSay(a delivery.CandidateAnswer, ran map[string]string) delivery.CandidateAnswer { + if a.Refused != "" || len(a.Differs) == 0 { + return a + } + kept := a.Differs[:0:0] + for _, d := range a.Differs { + if d.Composed != "" && ran[d.Module] == d.Composed { + a.Same = append(a.Same, d.Module) + continue + } + kept = append(kept, d) + } + a.Differs = kept + sort.Strings(a.Same) + a.Verdict = delivery.VerdictOf(a) + return a } // shadowBuilds is the build each module placed is composed at in shadow: the one it runs, and for one the candidate