Files
mesh-delivery/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go
T
jschoubben 4e4601a1eb Take-in and forge contracts live with their owners: the indexer's takein package, the forge bridge's forgemsg package; mesh-sdk pinned at main (issue 1585, ADR 0633)
The operator, 2026-11-04: logic one module owns lives inside that module as a package of its own, so the take-in step
changes no mesh-sdk. TakeIn, its keys and event names, PendingWait and PendingAnswer move to
modules/mesh-delivery-indexer/takein (its merges a takein.Merge of its own); Forge whole, with its group fields,
ForgeGroupEnd and GroupMerge, moves to modules/mesh-delivery-forge-bridge/forgemsg. Both are nested Go modules that their
readers take by replace, as buildrun is. Every importer moves in this commit: the indexer, the bridge, the watchman and
the view. Forge's private checks (the repository name, the version) are copied small; mesh-sdk main still holds an unused
Forge until its own fold removes it.

The indexer, bridge, watchman, view and checker pin mesh-sdk 5a38a12 (origin/main), no feat/take-in commit.

The bridge's integration bus now grants the state end-asks that bridge_group_end_again added: without it every test of
the package failed on a permissions violation. Test budget: two new test functions (TestForge, TestTakeInEvents) and the
two the join added are paid by merging four pairs in internal/bridge; the repository stays at 806 unit tests.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 00:05:01 +01:00

183 lines
10 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/provisions"
"mesh-delivery-view/internal/view"
)
// 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 := delivery.EncodeModule(delivery.Module{FP2: f, Manifest: manifest, Src: src, FPSrc: 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 delivery.BuilderBuild) string {
v, err := delivery.EncodeBuilderBuild(b)
return encoded(t, v, err)
}
want := func(w delivery.Want) string {
v, err := delivery.EncodeWant(w)
return encoded(t, v, err)
}
have := func(h delivery.Have) string {
v, err := delivery.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(delivery.BuilderBuildKey("db", fp("a"))): build(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: manifest}),
key(delivery.BuilderBuildKey("web", fp("b"))): build(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: manifest}),
key(delivery.BuilderBuildKey("lib", fp("c"))): build(delivery.BuilderBuild{State: delivery.BuildQueued, Waits: "@novox/x@1.2.0"}),
}.reader().State
r.WantsState = fake{
key(delivery.WantKey("ace", "db")): want(delivery.Want{FP: fp("a"), Self: true}),
key(delivery.WantKey("ace", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.RuleProvisions, Words: "the provision ace.web.database of web on ace is asked, not provided"}),
key(delivery.WantKey("g14", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.RuleCanary, Words: "the canary"}),
}.reader().State
r.WantsState.History = func(string) ([]view.Revision, error) { return []view.Revision{{At: since}}, nil }
r.HavesState = fake{key(delivery.HaveKey("ace", "db")): have(delivery.Have{FP: fp("a"), Phase: delivery.PhaseRunning, Health: delivery.HealthOK})}.reader().State
p, err := provisions.Encode(provisions.Provision{Provision: "database", Consumer: provisions.End{Module: "web", Node: "ace"},
Provider: provisions.End{Module: "db", Node: "ace"}, State: provisions.Asked, Why: "the build db runs does not offer it", At: since})
r.ProvisionsState = fake{key(provisions.Key("ace", "web", "database", "")): encoded(t, p, err)}.reader().State
off, err := delivery.EncodePresence(delivery.Presence{Online: false})
r.PresenceState = fake{key(delivery.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 != delivery.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(delivery.WantKey("ace", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.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)
}
}