Files
mesh-delivery/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go
T

187 lines
11 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"mesh-delivery-view/internal/view"
"reflect"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
// encoded is v as its contract's encoder writes it, failing the test when it refuses.
func encoded(t *testing.T, v []byte, err error) string {
t.Helper()
if err != nil {
t.Fatal(err)
}
return string(v)
}
// short is a fingerprint as the assigner's words shorten it.
func short(fp string) string { return fp[:len("src2:")+12] }
// tiersToolSaysEachModulesWait: view_tiers orders a source revision's modules by the module graph and names each one's
// wait (ADR 0603 §6): a consumer after its provider, held by the assigner's provision rule and its provision not provided;
// a module whose build waits for a package; a provider with no wait; a node offline named away; older modules left out.
func tiersToolSaysEachModulesWait(t *testing.T) {
fp := func(c string) string { return "src2:" + strings.Repeat(c, 64) }
manifest := strings.Repeat("d", 64)
mod := func(f string, src uint64) string {
b, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: f, Manifest: manifest, Src: int64(src), FpSrc: int64(src), Repository: "novox/mesh-catalog", Commit: "c"})
return encoded(t, b, err)
}
node := func(g graph.Node) string {
b, err := graph.Encode(g)
return encoded(t, b, err)
}
build := func(b builds.BuilderBuild) string {
v, err := builds.EncodeBuilderBuild(b)
return encoded(t, v, err)
}
want := func(w meshwantsclient.Want) string {
v, err := meshwantsclient.EncodeWant(w)
return encoded(t, v, err)
}
have := func(h nodeengine.HaveByNodeModuleValue) string {
v, err := nodeengineclient.EncodeHave(h)
return encoded(t, v, err)
}
key := func(k string, err error) string {
if err != nil {
t.Fatal(err)
}
return k
}
since := time.Date(2026, 11, 3, 16, 40, 0, 0, time.UTC)
r := fake{"db": mod(fp("a"), 9), "web": mod(fp("b"), 9), "lib": mod(fp("c"), 9), "old": mod(fp("e"), 5)}.reader()
r.GraphState = fake{
"db": node(graph.Node{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}),
"web": node(graph.Node{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database"}, Awaits: []string{"database"}}),
"lib": node(graph.Node{FP: fp("c"), Repository: "novox/mesh-catalog"}),
}.reader().State
r.Builds = fake{
key(builds.BuilderBuildKey("db", fp("a"))): build(builds.BuilderBuild{State: builds.BuildDone, Manifest: manifest}),
key(builds.BuilderBuildKey("web", fp("b"))): build(builds.BuilderBuild{State: builds.BuildDone, Manifest: manifest}),
key(builds.BuilderBuildKey("lib", fp("c"))): build(builds.BuilderBuild{State: builds.BuildQueued, Waits: "@novox/x@1.2.0"}),
}.reader().State
r.WantsState = fake{
key(meshwantsclient.WantKey("ace", "db")): want(meshwantsclient.Want{Fp: fp("a"), Self: true}),
key(meshwantsclient.WantKey("ace", "web")): want(meshwantsclient.Want{Self: true, Fp: fp("f"), Newest: fp("b"), Rule: meshwantsclient.RuleProvisions, Words: "the provision ace.web.database of web on ace is asked, not provided"}),
key(meshwantsclient.WantKey("g14", "web")): want(meshwantsclient.Want{Self: true, Fp: fp("f"), Newest: fp("b"), Rule: meshwantsclient.RuleCanary, Words: "the canary"}),
}.reader().State
r.WantsState.History = func(string) ([]view.Revision, error) { return []view.Revision{{At: since}}, nil }
r.HavesState = fake{key(nodeengineclient.HaveKey("ace", "db")): have(nodeengine.HaveByNodeModuleValue{Fp: fp("a"), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})}.reader().State
p, err := meshprovisionerclient.Encode(meshprovisionerclient.Provision{Provision: "database", Consumer: meshprovisionerclient.End{Module: "web", Node: "ace"},
Provider: meshprovisionerclient.End{Module: "db", Node: "ace"}, State: meshprovisionerclient.Asked, Why: "the build db runs does not offer it", At: since})
r.ProvisionsState = fake{key(meshprovisionerclient.Key("ace", "web", "database", "")): encoded(t, p, err)}.reader().State
off, err := meshwatchdogsclient.EncodeNodePresence(meshwatchdogsclient.NodePresence{Online: false})
r.PresenceState = fake{key(meshwatchdogsclient.PresenceNodeKey("g14")): encoded(t, off, err)}.reader().State
out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
got, _ := out.(view.Tiers)
if err != nil || got.Revision != 9 || got.Modules != 3 || len(got.Tiers) != 2 || !reflect.DeepEqual(got.Away, []string{"g14"}) || got.Graph != "" {
t.Fatalf("view_tiers 9 = %+v, %v", out, err)
}
first := fmt.Sprint(got.Tiers[0])
if len(got.Tiers[0]) != 2 || got.Tiers[0][0].Module != "db" || len(got.Tiers[0][0].Waits) != 0 || got.Tiers[0][1].Module != "lib" ||
len(got.Tiers[0][1].Waits) != 1 || !strings.Contains(got.Tiers[0][1].Waits[0].Words, "waiting for the package @novox/x@1.2.0") {
t.Errorf("the first tier = %s, want db with no wait and lib's build waiting for its package", first)
}
web := got.Tiers[1][0]
if web.Module != "web" || !reflect.DeepEqual(web.After, []string{"db"}) || len(web.Waits) != 2 ||
web.Waits[0].Edge != "want" || web.Waits[0].Node != "ace" || web.Waits[0].Rule != meshwantsclient.RuleProvisions || !web.Waits[0].Since.Equal(since) ||
web.Waits[1].Edge != "provision database" || !strings.Contains(web.Waits[1].Words, "asked: the build db runs does not offer it") {
t.Errorf("web = %+v, want after db, held on ace by the provision rule and its provision asked (g14 is away)", web)
}
// ADR 0633 §5 (1585/T24): a module the indexer holds back, read through indexer_pending, waits on the edge take-in from the
// wait's since; a held module the revision does not move is named in the first tier with that wait alone, a group's wait
// says its group, and a tool that cannot be asked is said and names no module.
heldSince := time.Date(2026, 11, 4, 22, 0, 0, 0, time.UTC)
r.Pending = func() (takein.PendingAnswer, error) {
return takein.PendingAnswer{Waits: []takein.PendingWait{
{Key: "m.00000000000000000042", What: takein.TakeInRevision, WaitsFor: "src2:abc", Since: heldSince, Holds: []string{"web", "newmod"}},
{Key: "g.feat", What: takein.TakeInGroup, Group: "feat", Since: heldSince.Add(time.Hour), Holds: []string{"lib"}}}}, nil
}
out, err = run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
held, _ := out.(view.Tiers)
if err != nil || len(held.Tiers) != 2 || held.Pending != "" {
t.Fatalf("view_tiers with waits = %+v, %v", out, err)
}
web = held.Tiers[1][0]
if last := web.Waits[len(web.Waits)-1]; last.Edge != view.TakeInEdge || !last.Since.Equal(heldSince) || !strings.Contains(last.Words, "src2:abc") {
t.Errorf("web = %+v, want its last wait on the edge take-in since %s", web, heldSince)
}
if lib := held.Tiers[0][1]; lib.Module != "lib" || lib.Waits[len(lib.Waits)-1].Edge != view.TakeInEdge || !strings.Contains(lib.Waits[len(lib.Waits)-1].Words, "group feat") {
t.Errorf("lib = %+v, want its last wait the group's", lib)
}
if nm := held.Tiers[0][len(held.Tiers[0])-1]; nm.Module != "newmod" || len(nm.Waits) != 1 || nm.Waits[0].Edge != view.TakeInEdge {
t.Errorf("the first tier = %+v, want newmod, which the revision does not move, named with its take-in wait alone", held.Tiers[0])
}
r.Pending = func() (takein.PendingAnswer, error) { return takein.PendingAnswer{}, errors.New("no answer") }
if out, err = run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}); err != nil || !strings.Contains(out.(view.Tiers).Pending, "no answer") ||
strings.Contains(fmt.Sprint(out.(view.Tiers).Tiers), view.TakeInEdge) {
t.Errorf("view_tiers with no answer of indexer_pending = %+v, %v", out, err)
}
r.Pending = nil
// No revision named: the gate's newest, its source key's revision.
r.SourceState = view.StateRead{GetEntry: func(string) (json.RawMessage, uint64, error) { return json.RawMessage(`{}`), 9, nil }}
if out, err := run(t, r, view.TiersTool, map[string]any{}); err != nil || out.(view.Tiers).Revision != 9 || out.(view.Tiers).Modules != 3 {
t.Errorf("view_tiers of the gate's newest = %+v, %v", out, err)
}
// ADR 0614 §2-§5: a want held on a verb, a reader capability or bus state is named by its edge; bus state is no
// provisioner key (none is ever made for it), so a module asking it shows no provision wait of it; and each verb edge
// the graph leaves out on a cycle is named.
r.GraphState = fake{
"db": node(graph.Node{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}),
"web": node(graph.Node{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database", graph.BusState}, Awaits: []string{"database", graph.BusState},
Invokes: []string{"seat:x.v", "db.loop"}, LeftOut: []string{"db.loop"}}),
"lib": node(graph.Node{FP: fp("c"), Repository: "novox/mesh-catalog"}),
}.reader().State
for words, edge := range map[string]string{
graph.VerbWaitWords("seat:x.v", "web", "ace"): "verb seat:x.v",
graph.ReaderWaitWords("ace", []string{"tool-runner"}, "web", short(fp("b"))): "reader capability tool-runner",
"the provision ace.web.database of web on ace is asked, not provided": "want",
} {
r.WantsState = fake{key(meshwantsclient.WantKey("ace", "web")): want(meshwantsclient.Want{Self: true, Fp: fp("f"), Newest: fp("b"), Rule: meshwantsclient.RuleProvisions, Words: words})}.reader().State
out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
got, _ := out.(view.Tiers)
if err != nil || len(got.Tiers) != 2 {
t.Fatalf("view_tiers 9 = %+v, %v", out, err)
}
web := got.Tiers[1][0]
if len(web.Waits) != 2 || web.Waits[0].Edge != edge || web.Waits[1].Edge != "provision database" ||
!reflect.DeepEqual(web.LeftOut, []string{"db.loop"}) {
t.Errorf("web held by %q = %+v, want its want named %s, no wait for a bus-state key and db.loop left out", words, web, edge)
}
}
// The graph not readable: every module in the first tier, said; past 500 modules the answer says it is cut short.
r.GraphState = view.StateRead{Keys: func() ([]string, error) { return nil, errors.New("no bucket") }, Get: r.GraphState.Get}
if out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}); err != nil || len(out.(view.Tiers).Tiers) != 1 ||
!strings.Contains(out.(view.Tiers).Graph, "cannot be read") {
t.Errorf("view_tiers with no graph = %+v, %v", out, err)
}
many := fake{}
for i := 0; i < view.TiersMost+2; i++ {
many[fmt.Sprintf("m%03d", i)] = mod(fp("a"), 9)
}
r.State = many.reader().State
if out, err := run(t, r, view.TiersTool, map[string]any{"revision": "9"}); err != nil || out.(view.Tiers).Modules != view.TiersMost+2 ||
!strings.Contains(out.(view.Tiers).Cut, "the first 500") {
t.Errorf("view_tiers past its bound = %+v, %v", out.(view.Tiers).Cut, err)
}
}