The node's words for a want it composes nothing of are said once per reason, and proved on the live paths (issue 1966)
The said-once keys no longer hold where the indexer's picture stands; a module composed from the manifest for want of a done build is named in Pass.Unmade; the agent's wait is told once per want and again after the composition was there; the two tests are folded into existing ones. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -280,6 +280,72 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
b.mu.Unlock()
|
||||
t.Fatalf("handed %v, want %s and %s", got, lemursKey, nodeKey)
|
||||
}
|
||||
// Issue 1966 (novox, 2026-11-09): every pass names each wanted module it composed nothing of, or composed from the indexer's
|
||||
// manifest for want of a done build, with why, and a want it did not take; said once; the agent's word that it waits
|
||||
// hands a composition the node believes handed again.
|
||||
tfp := meshcatalogclient.SourcePrefix2 + strings.Repeat("7", 64)
|
||||
tool := `{"module":"tool","version":"1","capabilities":["systemd"],"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},` +
|
||||
`"resources":[{"id":"service","type":"service","unit":"tool.service","state":"running","boot":"enabled"}]}`
|
||||
tdigest := meshcatalogclient.ManifestDigest([]byte(tool))
|
||||
tman, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "tool", Manifest: tool})
|
||||
tkey, _ := meshcatalogclient.ManifestsKey(tdigest)
|
||||
b.mu.Lock()
|
||||
b.kv[buckets.Manifests+"/"+tkey] = tman
|
||||
b.mu.Unlock()
|
||||
tmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: tfp, Manifest: tdigest, Repository: "novox/t", Commit: "c"})
|
||||
c.takeModule(agent.Update{Key: "tool", Value: tmod, Revision: 9})
|
||||
for module, w := range map[string]meshwants.WantsByWantNodeModuleValue{"tool": {Fp: tfp, Self: true}, "ghost": {Fp: fp, Self: true}, "nofp": {Self: true}} {
|
||||
raw, _ := bucketstest.Want(w)
|
||||
c.takeWant(agent.Update{Key: "want.n1." + module, Value: raw})
|
||||
}
|
||||
c.mu.Lock()
|
||||
placed := c.assignments["n1"]
|
||||
placed.Modules = append(slices.Clone(placed.Modules), meshmoduleassigner.PlacedModule{Module: "tool"})
|
||||
c.assignments["n1"] = placed
|
||||
c.mu.Unlock()
|
||||
said := func(sub string) (n int) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for _, l := range b.said {
|
||||
if strings.Contains(l, sub) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
c.compose(ctx)
|
||||
why := map[string]string{"tool": c.WhyUnmade("tool"), "ghost": c.WhyUnmade("ghost"), "nofp": c.WhyUnmade("nofp"), "lemurs": c.WhyUnmade("lemurs")}
|
||||
if !strings.Contains(why["tool"], "the builder holds no done build of it at "+tfp) || !strings.Contains(why["ghost"], "places no ghost") ||
|
||||
!strings.Contains(why["nofp"], "want not taken: its want names no fingerprint") || why["lemurs"] != "" {
|
||||
t.Errorf("the pass says it composed nothing of %q", why)
|
||||
}
|
||||
// An update of another module moves the picture's revision: the same reasons are not said again.
|
||||
c.takeModule(agent.Update{Key: "lemurs", Value: mod, Revision: 10})
|
||||
c.compose(ctx)
|
||||
if n := said("the node composes nothing of ghost"); n != 1 {
|
||||
t.Errorf("the journal says why ghost is not composed %d times, want once", n)
|
||||
}
|
||||
// The state of this step is taken away again, and the pass that takes back what it handed is made here.
|
||||
for _, module := range []string{"tool", "ghost", "nofp"} {
|
||||
c.takeWant(agent.Update{Key: "want.n1." + module, Deleted: true})
|
||||
}
|
||||
c.takeModule(agent.Update{Key: "tool", Deleted: true})
|
||||
c.mu.Lock()
|
||||
placed.Modules = placed.Modules[:len(placed.Modules)-1]
|
||||
c.assignments["n1"] = placed
|
||||
c.mu.Unlock()
|
||||
c.compose(ctx)
|
||||
// The agent waits for what the node believes it handed: handed again; for what it never handed, nothing is.
|
||||
c.Waiting("ghost", fp)
|
||||
select {
|
||||
case u := <-out:
|
||||
t.Errorf("handed %s for a wait of a composition never handed", u.Key)
|
||||
default:
|
||||
}
|
||||
c.Waiting("lemurs", fp)
|
||||
if u := next(t, out); !strings.Contains(u.Key, "lemurs") {
|
||||
t.Errorf("handed %s, want lemurs's composition again", u.Key)
|
||||
}
|
||||
// ADR 0601 §1: the module composed today at the build it runs, as the agent compares it with what runs. The same inputs give
|
||||
// the digest handed; a changed input (here its manifest's service, as a changed setting would change a file) another; a
|
||||
// build that cannot be composed (no done build or manifest of it: held) gives none.
|
||||
@@ -2090,59 +2156,3 @@ func (b *patternBus) putAssigned(vals map[string][]byte, rev uint64) {
|
||||
b.put(buckets.Assignments, k, vals[k], rev)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1966 (novox, 2026-11-09): a want the composer made nothing of was said nowhere, and a composition it believed
|
||||
// handed that the agent never held stayed unhanded. Every pass names each wanted module it composed nothing of, with why,
|
||||
// in Pass.Unmade and as a journal line; a want not taken is said; and the agent's word that it waits hands the
|
||||
// composition again.
|
||||
func TestAWantTheNodeMakesNothingOfSaysWhyAndAWaitingAgentIsHandedItAgain(t *testing.T) {
|
||||
b := newFakeBus()
|
||||
fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64)
|
||||
gfp := meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64)
|
||||
digest := meshcatalogclient.ManifestDigest([]byte(lemurs))
|
||||
man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "lemurs", Manifest: lemurs})
|
||||
key, _ := meshcatalogclient.ManifestsKey(digest)
|
||||
b.kv[buckets.Manifests+"/"+key] = man
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
c := New(b.env())
|
||||
out, err := c.Watch(ctx, "n1.>")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
place := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}})
|
||||
have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
||||
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
||||
want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true})
|
||||
ghost, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: gfp, Self: true})
|
||||
nofp, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Self: true})
|
||||
b.putAssigned(t, place)
|
||||
b.put(t, buckets.Have, "n1.node", have)
|
||||
b.put(t, buckets.Modules, "lemurs", mod)
|
||||
b.put(t, buckets.Wants, "want.n1.lemurs", want)
|
||||
b.put(t, buckets.Wants, "want.n1.ghost", ghost)
|
||||
b.put(t, buckets.Wants, "want.n1.nofp", nofp)
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Assignments, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
b.send(bucket, agent.Update{Caught: true})
|
||||
}
|
||||
for !next(t, out).Caught {
|
||||
}
|
||||
why := map[string]string{"ghost": c.WhyUnmade("ghost"), "nofp": c.WhyUnmade("nofp"), "lemurs": c.WhyUnmade("lemurs")}
|
||||
if !strings.Contains(why["ghost"], "places no ghost") || !strings.Contains(why["nofp"], "names no fingerprint") || why["lemurs"] != "" {
|
||||
t.Errorf("the pass says it composed nothing of %q", why)
|
||||
}
|
||||
b.mu.Lock()
|
||||
said := strings.Join(b.said, "\n")
|
||||
b.mu.Unlock()
|
||||
if !strings.Contains(said, "the node composes nothing of ghost: ") {
|
||||
t.Errorf("the journal does not say why ghost is not composed: %q", said)
|
||||
}
|
||||
// The agent waits for what the node believes it handed: handed again, and a wait for what was never handed does nothing.
|
||||
c.Waiting("ghost", gfp)
|
||||
quiet(t, out)
|
||||
c.Waiting("lemurs", fp)
|
||||
if u := next(t, out); !strings.Contains(u.Key, "lemurs") {
|
||||
t.Errorf("handed %s, want lemurs's composition again", u.Key)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user