package buildrun import ( "bytes" "context" "errors" "fmt" "log" "strings" "testing" "time" "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient" meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep" ) func gateReport(t *testing.T, node, version string, at time.Time) (string, []byte) { t.Helper() key, _ := buildstep.ToolchainKey(node, "typescript") value, err := buildstep.EncodeToolchain(buildstep.Toolchain{Language: "typescript", Version: version, Recipe: "f59571cf4be75adf", At: at}) if err != nil { t.Fatal(err) } return key, value } // placing is a value of the state `assignments` placing modules on node. func agreedOf(set *ToolchainSet) (string, string) { tc, none := buildstep.AgreedToolchain(set.Entries("typescript")) return tc.Version, none } // Issue 1940, the gate over the catalogue's assignments (issue 774, ADR 0549 ยง3): a build node's report counts only while // the assignments places its build module on its node, and only once written since; before the assignments is settled, and // while no node's assignments is held, every report counts. The live cases: mesh-build-node taken off ace left // ace.typescript at v26.12.0 against v26.12.1 on the three others; put on ace again, its report of 2026-10-23 06:29 came // back before the module ran there. func TestToolchainGate(t *testing.T) { old := time.Date(2026, 10, 23, 6, 29, 18, 0, time.UTC) // ace's report from its earlier run placed := time.Date(2026, 10, 25, 19, 56, 45, 0, time.UTC) // mesh-build-node placed on ace again three := []string{"g14", "novox", "shanks"} start := func(t *testing.T, seeded bool) (*ToolchainGate, *ToolchainSet, *[]string) { set := &ToolchainSet{Seats: testSeats()} said := &[]string{} gate := NewToolchainGate([]string{testGo, testNode}, set.Put, func(f string, a ...any) { *said = append(*said, fmt.Sprintf(f, a...)) }) for _, node := range three { if seeded { key, value := placing(t, node, testNode) gate.Assignments(key, value, false, old.Add(-time.Hour)) } key, value := gateReport(t, node, "v26.12.1", placed.Add(time.Minute)) gate.Toolchain(testNode, key, value, false) } return gate, set, said } t.Run("not placed on ace: its report is no builder's", func(t *testing.T) { gate, set, said := start(t, true) key, value := gateReport(t, "ace", "v26.12.0", old) gate.Toolchain(testNode, key, value, false) if a := buildstep.ToolchainAgreement(set.Entries("typescript")); len(a.SittingOut) != 1 || a.SittingOut[0].Node != "ace" { // ADR 0584: the majority is agreed, ace's report counts as the one that sits out t.Fatalf("before the assignments is settled every report counts; got %+v", a) } k, v := placing(t, "ace", testGo) // another build module on ace: no matter gate.Assignments(k, v, false, old) if !gate.Settled() { t.Fatal("settling left ace's report out, so what was passed on changed") } if v, none := agreedOf(set); v != "v26.12.1" || none != "" { t.Fatalf("the placed nodes agree on v26.12.1; got %q, %q", v, none) } if len(*said) != 1 || !strings.Contains((*said)[0], "does not place mesh-build-node on ace") { t.Fatalf("ace's report is said left out once; said %q", *said) } if gate.Toolchain(testNode, key, value, false) { t.Fatal("a report of a node not placed changed what was passed on") } // Placed on ace with a report written since: it counts, and the builders really disagree. k, v = placing(t, "ace", testGo, testNode) gate.Assignments(k, v, false, old.Add(-time.Minute)) if a := buildstep.ToolchainAgreement(set.Entries("typescript")); a.Toolchain.Version != "v26.12.1" || len(a.SittingOut) != 1 || a.SittingOut[0].Version != "v26.12.0" { t.Fatalf("builders that run different versions: the majority is agreed and ace sits out; got %+v", a) } // Taken off ace again (its key deleted): its report goes again. if !gate.Assignments(k, nil, true, placed) { t.Fatal("taking ace's assignments away passed nothing on") } if v, none := agreedOf(set); v != "v26.12.1" || none != "" { t.Fatalf("got %q, %q", v, none) } // A report deleted from the bucket goes whether its node is placed or not. key, _ = gateReport(t, "g14", "v26.12.1", placed) if !gate.Toolchain(testNode, key, nil, true) || len(set.Entries("typescript")) != 2 { t.Fatalf("deleting g14's report: %d entries left, want novox and shanks", len(set.Entries("typescript"))) } }) t.Run("build modules given later pass their reports on when the assignments moves", func(t *testing.T) { set := &ToolchainSet{Seats: testSeats()} gate := NewToolchainGate(nil, set.Put, func(string, ...any) {}) gate.Settled() k, v := placing(t, "novox", testGo) gate.Assignments(k, v, false, old) key, value := gateReport(t, "ace", "v26.12.1", placed.Add(time.Minute)) gate.Toolchain(testNode, key, value, false) // ace is not placed: left out k, v = placing(t, "ace", testNode) if gate.Assignments(k, v, false, placed) || len(set.Entries("typescript")) != 0 { t.Fatalf("a module the gate was not given passed on at the assignments: %v", set.Entries("typescript")) } if !gate.UseModules([]string{testNode}) || len(set.Entries("typescript")) != 1 { t.Fatalf("given mesh-build-node, ace's report placed since is passed on; entries %v", set.Entries("typescript")) } }) for _, c := range []struct { name string at time.Time // when ace wrote its report counts bool }{ {"a report left before the assignments", old, false}, {"a report written since the assignments", placed.Add(30 * time.Second), true}, } { t.Run(c.name, func(t *testing.T) { gate, set, said := start(t, true) k, v := placing(t, "ace") // ace's key, first heard placing nothing gate.Assignments(k, v, false, old.Add(-time.Hour)) gate.Settled() k, v = placing(t, "ace", testNode) if gate.Assignments(k, v, false, placed) { t.Fatal("assignments with no report passed something on") } key, value := gateReport(t, "ace", "v26.12.0", c.at) if got := gate.Toolchain(testNode, key, value, false); got != c.counts { t.Fatalf("ace's report passed on: %v, want %v", got, c.counts) } // ace's assignments written again with another module beside: its standing does not move, nothing is said again. k, v = placing(t, "ace", testGo, testNode) if gate.Assignments(k, v, false, placed.Add(time.Minute)) { t.Fatal("the same assignments of mesh-build-node changed what was passed on") } if c.counts { return } if v, none := agreedOf(set); v != "v26.12.1" || none != "" { t.Fatalf("ace's left report is no builder's; got %q, %q", v, none) } if len(*said) != 1 || !strings.Contains((*said)[0], "before mesh-build-node was placed on ace at 2026-10-25T19:56:45Z") { t.Fatalf("said %q, want once why ace's report is left out", *said) } key, value = gateReport(t, "ace", "v26.12.1", placed.Add(time.Minute)) if !gate.Toolchain(testNode, key, value, false) || len(set.Entries("typescript")) != 4 { t.Fatal("ace's new report was not passed on") } }) } t.Run("no assignments held: every report counts", func(t *testing.T) { gate, set, _ := start(t, false) key, value := gateReport(t, "ace", "v26.12.0", old) gate.Toolchain(testNode, key, value, false) gate.Settled() if n := len(set.Entries("typescript")); n != 4 { t.Fatalf("before the first assignments file every report counts; got %d", n) } k, v := placing(t, "g14", testNode) if !gate.Assignments(k, v, false, old.Add(-time.Hour)) || len(set.Entries("typescript")) != 1 { t.Fatalf("the first assignments leaves every node it does not place out; got %d", len(set.Entries("typescript"))) } if !gate.Assignments(k, nil, true, placed) || len(set.Entries("typescript")) != 4 { t.Fatal("the last assignments gone: every report counts again") } }) // Design 47 *Each part acts on its own entries*: fed by the assignment half's revisions, the gate settles at the first, // leaves out a node no revision places the build module on, and counts a node placed in a later revision only by a // report written since that revision's time. t.Run("fed by the assignment half", func(t *testing.T) { set := &ToolchainSet{Seats: testSeats()} said := &[]string{} gate := NewToolchainGate([]string{testGo, testNode}, set.Put, func(f string, a ...any) { *said = append(*said, fmt.Sprintf(f, a...)) }) for _, node := range append([]string{"ace"}, three...) { key, value := gateReport(t, node, "v26.12.1", placed.Add(time.Minute)) if node == "ace" { key, value = gateReport(t, node, "v26.12.0", old) } gate.Toolchain(testNode, key, value, false) } revision := func(n int64, at time.Time, nodes ...string) meshwants.AssignmentRevision { r := meshwants.AssignmentRevision{Revision: n, Kind: "trunk", At: &at} for _, node := range nodes { r.Entries = append(r.Entries, meshwants.NodeEntryRef{Node: node, Module: testNode, Digest: "d-" + node}) } return r } if !gate.Revision(revision(7, old.Add(-time.Hour), three...)) || len(set.Entries("typescript")) != 3 { t.Fatalf("the first revision settles the gate and leaves ace out; %d entries", len(set.Entries("typescript"))) } if gate.Revision(revision(7, old.Add(-time.Hour), three...)) { t.Fatal("the same revision again changed what was passed on") } if gate.Revision(revision(8, placed, "ace", "g14", "novox", "shanks")); len(set.Entries("typescript")) != 3 { t.Fatal("ace's report from before revision 8 placed it counts") } if len(*said) < 2 || !strings.Contains((*said)[len(*said)-1], "before mesh-build-node was placed on ace at 2026-10-25T19:56:45Z") { t.Fatalf("said %q, want why ace's old report is left out", *said) } key, value := gateReport(t, "ace", "v26.12.1", placed.Add(time.Minute)) if !gate.Toolchain(testNode, key, value, false) || len(set.Entries("typescript")) != 4 { t.Fatal("ace's report written since revision 8 is not passed on") } if !gate.Revision(revision(9, placed.Add(time.Hour), "ace", "novox", "shanks")) || len(set.Entries("typescript")) != 3 || len(gate.Nodes()) != 3 { t.Fatalf("g14 taken off in revision 9: %d entries, nodes %v", len(set.Entries("typescript")), gate.Nodes()) } }) t.Run("the history: a module in the first revision heard of a key stands since before it", func(t *testing.T) { p := meshmoduleassignerclient.NewPlaced() // ace's node key is the first revision heard of it: its module stands since before. k, v := placing(t, "ace", testNode) p.Put(k, v, false, old) if _, changed, _ := p.Put("ace."+testNode, []byte("{}"), false, placed); len(changed) != 0 { t.Errorf("a module key says which modules stand: %v", changed) } k2, v2 := placing(t, "g14") p.Put(k2, v2, false, old) k2, v2 = placing(t, "g14", testNode) if _, changed, _ := p.Put(k2, v2, false, placed); strings.Join(changed, ",") != testNode { t.Errorf("a module placed says changed %v", changed) } if _, _, err := p.Put("novox.node", []byte("not json"), false, old); err == nil { t.Error("a value that does not read was not said") } if s, ok := p.Since("ace", testNode); !ok || !s.IsZero() { t.Errorf("ace's module stands since %v, %v; want the zero time", s, ok) } if s, ok := p.Since("g14", testNode); !ok || !s.Equal(placed) { t.Errorf("g14's module stands since %v, %v; want the revision that placed it", s, ok) } if got := p.PlacedNodes(testNode); strings.Join(got, ",") != "ace,g14" || len(p.ByNode()) != 2 || !p.Seeded() { t.Errorf("PlacedNodes = %v, ByNode = %v", got, p.ByNode()) } }) t.Run("assignments the bus refuses is said, and the part goes on", func(t *testing.T) { var logs bytes.Buffer logger := log.New(&logs, "", 0) refused := errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_assignments") gate, _, _ := start(t, false) for _, bus := range []refusingBus{{err: refused}, {refuseOpen: true, err: refused}} { if _, err := FollowAssignmentsIn(context.Background(), bus, "acme-assigner_assignments", gate, func(context.Context) {}, logger.Printf); err != nil { t.Errorf("a refused assignments stopped the start: %v", err) } } if !strings.Contains(logs.String(), "cannot be watched") || !strings.Contains(logs.String(), "cannot be read") { t.Errorf("the refusals were not said: %q", logs.String()) } }) }