ADR 0593 §6, §7, §9: the integration test runs the node agent against the real bus under the grant the grants part composes for a node: it publishes web's composition in composed-<node> with the digest its have names, and answers compose-candidate for the indexer's candidate, changing nothing. A node that cannot compose now answers unready, so the checker counts it as no answer by the verdict and never by words. The golden grant reads the indexer's candidates and verdicts as it reads the builder's builds. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
126 lines
6.3 KiB
Go
126 lines
6.3 KiB
Go
package selfcompose
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
|
|
"github.com/novox/mesh-host/internal/agent"
|
|
)
|
|
|
|
// novox/hq ADR 0593 §7: the verb compose-candidate composes a check's candidate on this node in shadow. A candidate that
|
|
// changes a placed module composes it at the candidate's build, changed; one whose manifest the indexer refused is
|
|
// answered refused, naming the module and the judge's words, and the verdict is refused; a candidate the indexer does not
|
|
// hold is refused in words. Nothing is applied, nothing is written, no placement answer is handed, and the answer comes
|
|
// within the bound; a shadow not ready and an ended bound answer unready at once, which the checker takes as no answer.
|
|
func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
|
|
b := newFakeBus()
|
|
fp := delivery.SourcePrefix2 + strings.Repeat("a", 64)
|
|
fpNew := delivery.SourcePrefix2 + strings.Repeat("b", 64)
|
|
digest := delivery.ManifestDigest([]byte(lemurs))
|
|
lemurs2 := strings.Replace(lemurs, `"version":"1"`, `"version":"2"`, 1)
|
|
digest2 := delivery.ManifestDigest([]byte(lemurs2))
|
|
for d, body := range map[string]string{digest: lemurs, digest2: lemurs2} {
|
|
man, _ := delivery.EncodeManifest(delivery.Manifest{Module: "lemurs", JSON: body})
|
|
key, _ := delivery.ManifestsKey(d)
|
|
b.kv[delivery.ManifestsBucket+"/"+key] = man
|
|
}
|
|
candidateOf := func(manifest string) string {
|
|
head := delivery.Head{Repository: "novox/x", Number: 7, Head: "c0ffee" + manifest[len(manifest)-6:]}
|
|
heads, _ := delivery.HeadsHash([]delivery.Head{head})
|
|
pulls := []delivery.CandidatePull{{Head: head, Modules: []string{"lemurs"},
|
|
Fps: map[string]delivery.CandidateModule{"lemurs": {FP2: fpNew, Manifest: manifest}}}}
|
|
raw, err := delivery.EncodeCandidate(delivery.Candidate{Group: "novox/x#7", HeadsHash: heads, Pulls: pulls,
|
|
FPSet: delivery.FingerprintSet(pulls)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, _ := delivery.CompositionKey("novox/x#7", heads)
|
|
b.kv[delivery.CandidatesBucket+"/"+key] = raw
|
|
return key
|
|
}
|
|
accepted, refusedKey := candidateOf(digest2), ""
|
|
{
|
|
// The same change, its manifest refused by the indexer's judge.
|
|
refusedMan := strings.Replace(lemurs2, `"version":"2"`, `"version":"3"`, 1)
|
|
d3 := delivery.ManifestDigest([]byte(refusedMan))
|
|
man, _ := delivery.EncodeManifest(delivery.Manifest{Module: "lemurs", JSON: refusedMan})
|
|
key, _ := delivery.ManifestsKey(d3)
|
|
b.kv[delivery.ManifestsBucket+"/"+key] = man
|
|
vkey, _ := delivery.VerdictsKey(d3)
|
|
v, err := delivery.EncodeManifestVerdict(delivery.ManifestVerdict{Module: "lemurs", State: delivery.VerdictRefused,
|
|
Problems: []string{"a field the judge refuses"}, Judge: "src2:j", At: time.Now()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.kv[delivery.VerdictsBucket+"/"+vkey] = v
|
|
refusedKey = candidateOf(d3)
|
|
}
|
|
|
|
env := b.env()
|
|
state := &fakeComposed{kv: map[string]ComposedEntry{}}
|
|
env.ComposedState = state
|
|
var applied atomic.Int64
|
|
env.Refused = func(string, map[string]string) { applied.Add(1) }
|
|
var handed atomic.Int64
|
|
s := NewShadow(env, func(context.Context, map[string]delivery.CandidateAnswer) { handed.Add(1) })
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go func() { _ = s.Run(ctx) }()
|
|
if a := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: accepted}); a.Verdict != delivery.ComposeCandidateUnready || a.Refused == "" {
|
|
t.Errorf("a shadow not ready answered %+v", a)
|
|
}
|
|
place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
|
Modules: []delivery.PlacedModule{{Module: "lemurs"}}})
|
|
have, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
|
mod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
watching := func() int { b.mu.Lock(); defer b.mu.Unlock(); return len(b.watches) }
|
|
for watching() < 6 && time.Now().Before(deadline) {
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
b.put(t, delivery.PlacementBucket, "placement.n1", place)
|
|
b.put(t, delivery.HaveBucket, "n1.node", have)
|
|
b.put(t, delivery.ModulesBucket, "lemurs", mod)
|
|
for _, bucket := range []string{delivery.PlacementCandidatesBucket, delivery.PlacementBucket, delivery.HaveBucket, delivery.ModulesBucket, ResolvedBucket} {
|
|
b.send(bucket, agent.Update{Caught: true})
|
|
}
|
|
for !s.ready.Load() && time.Now().Before(deadline) {
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
|
|
a := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: accepted})
|
|
if _, err := delivery.EncodeComposeCandidateAnswer(a); err != nil {
|
|
t.Fatalf("the answer does not fit the seat's shape: %v (%+v)", err, a)
|
|
}
|
|
if a.Verdict != delivery.ComposeCandidateComposed || len(a.Modules) != 1 || a.Modules[0].Module != "lemurs" ||
|
|
a.Modules[0].State != delivery.ModuleComposedState || a.Modules[0].FP != fpNew || !a.Modules[0].Changed ||
|
|
a.Own.Module != delivery.HaveNodeWord || a.Took > delivery.ComposeAnswerBound.Milliseconds() {
|
|
t.Errorf("an accepted candidate answered %+v", a)
|
|
}
|
|
r := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: refusedKey})
|
|
if r.Verdict != delivery.ComposeCandidateRefused || len(r.Modules) != 1 || r.Modules[0].State != delivery.ModuleRefusedState ||
|
|
!strings.Contains(r.Modules[0].Words, "a field the judge refuses") {
|
|
t.Errorf("a candidate whose manifest the indexer refused answered %+v", r)
|
|
}
|
|
if _, err := delivery.EncodeComposeCandidateAnswer(r); err != nil {
|
|
t.Errorf("the refused answer does not fit the seat's shape: %v", err)
|
|
}
|
|
missing, _ := delivery.CompositionKey("novox/x#8", "0123456789ab")
|
|
if m := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: missing}); !strings.Contains(m.Refused, "holds no candidate") {
|
|
t.Errorf("a candidate the indexer does not hold answered %+v", m)
|
|
}
|
|
ended, end := context.WithCancel(ctx)
|
|
end()
|
|
if e := s.ComposeCandidate(ended, delivery.ComposeCandidateAsk{Candidate: accepted}); e.Verdict != delivery.ComposeCandidateUnready {
|
|
t.Errorf("an ended bound answered %+v", e)
|
|
}
|
|
if len(state.written()) != 0 || applied.Load() != 0 || handed.Load() != 0 {
|
|
t.Errorf("composing candidates wrote %v, marked %d have(s), handed %d answer(s); want nothing", state.written(), applied.Load(), handed.Load())
|
|
}
|
|
}
|