package view import ( "encoding/json" "errors" "fmt" "strconv" "strings" "testing" "time" ) // usageNow is the fixture's clock. var usageNow = time.Date(2026, 10, 26, 12, 0, 0, 0, time.UTC) // at is the fixture's time hh:mm on its day. func at(hm string) time.Time { t, _ := time.Parse("2006-01-02 15:04", "2026-10-26 "+hm) return t.UTC() } // stepID is a step id made at t, as buildstep.NewID makes it, with suffix as its six hex digits. func stepID(t time.Time, suffix string) string { return strconv.FormatInt(t.UnixMilli(), 36) + suffix } func states(m map[string]string) StateRead { return StateRead{ Keys: func() ([]string, error) { var k []string for key := range m { k = append(k, key) } return k, nil }, Get: func(key string) (json.RawMessage, error) { if v, ok := m[key]; ok { return json.RawMessage(v), nil } return nil, nil }, } } func step(node, id, tool, state string, started, ended time.Time) string { e := "" if !ended.IsZero() { e = `,"ended":"` + ended.Format(time.RFC3339) + `"` } return fmt.Sprintf(`{"v":1,"id":%q,"tool":%q,"node":%q,"state":%q,"started":%q%s}`, id, tool, node, state, started.Format(time.RFC3339), e) } // usageFixture is four nodes: shanks builds Go at capacity 2 (two asks running, one left running, a try's ask, a check, // a recheck of the morning and a hand step ended); ace holds the Go seat at capacity 2 and is paused; g14 holds it at // capacity 0; the Node seat on shanks has no resolved capacity and takes the modules' default. func usageFixture() UsageReader { running := `{"v":1,"phase":"running","health":"ok"}` tryAsk := stepID(at("11:30"), "a00003") goSteps := map[string]string{} add := func(node, id, tool, state string, started, ended time.Time) { goSteps[node+"."+id] = step(node, id, tool, state, started, ended) } add("shanks", stepID(at("11:50"), "a00001"), "delivery ask", "running", at("11:50"), time.Time{}) add("shanks", stepID(at("11:55"), "a00002"), "check ask", "running", at("11:55"), time.Time{}) add("shanks", tryAsk, "check ask", "passed", at("11:30"), at("11:40")) add("shanks", stepID(at("11:20"), "a00004"), "check ask", "failed", at("11:20"), at("11:26")) add("shanks", stepID(at("06:00"), "a00005"), "recheck ask", "passed", at("06:00"), at("06:10")) add("shanks", stepID(at("10:00"), "a00006"), "check ask", "running", at("10:00"), time.Time{}) add("shanks", stepID(at("11:45"), "a00007"), "build_go_test", "passed", at("11:45"), at("11:46")) // Two keys a build module ended for being left running: their end is when it noticed, so neither is finished work. goSteps["shanks."+stepID(at("08:00"), "a00008")] = `{"v":1,"id":"` + stepID(at("08:00"), "a00008") + `","tool":"check ask","node":"shanks","state":"failed","started":"2026-10-26T08:00:00Z","ended":"2026-10-26T11:59:00Z","reason":"the build module restarted while it ran"}` goSteps["shanks."+stepID(at("09:00"), "a00009")] = `{"v":1,"id":"` + stepID(at("09:00"), "a00009") + `","tool":"delivery ask","node":"shanks","state":"failed","started":"2026-10-26T09:00:00Z","ended":"2026-10-26T11:58:00Z","reason":"past the step bound: its asker stopped waiting at 2026-10-26T09:30:00Z"}` goSteps["shanks.notastepkey"] = `{}` // a key that is no step id is passed over return UsageReader{ Now: func() time.Time { return usageNow }, Haves: states(map[string]string{"shanks.mesh-build-go": running, "shanks.mesh-build-node": running, "ace.mesh-build-go": running, "g14.mesh-build-go": running, "g14.mesh-build-node": running, "shanks.node": `{}`, "shanks.mesh-delivery-view": running}), Resolved: states(map[string]string{ "shanks.mesh-build-go.capacity": `{"node":"shanks","module":"mesh-build-go","key":"capacity","value":"shanks=2,ace=2,g14=0,novox=0","source":"mesh"}`, "ace.mesh-build-go.capacity": `{"node":"ace","module":"mesh-build-go","key":"capacity","value":"shanks=2,ace=2,g14=0,novox=0","source":"mesh"}`, "g14.mesh-build-go.capacity": `{"node":"g14","module":"mesh-build-go","key":"capacity","value":"shanks=2,ace=2,g14=0,novox=0","source":"mesh"}`, "shanks.mesh-config.other": `{"value":"x"}`, "g14.mesh-build-node.capacity": `{"node":"g14","module":"mesh-build-node","key":"capacity","source":"unreadable"}`, }), Steps: map[string]StateRead{"go": states(goSteps), "node": states(map[string]string{}), "image": states(map[string]string{})}, Pauses: map[string]StateRead{"go": states(map[string]string{"paused.ace": `{"v":1,"paused":true,"by":"lead@shanks","at":"2026-10-26T09:00:00Z"}`}), "node": states(nil), "image": states(nil)}, Tries: states(map[string]string{ "try-" + strconv.FormatInt(at("11:29").UnixNano(), 10): `{"id":"try-1","repository":"novox/mesh-sdk","head":"h","trunk":"t","state":"pass","asks":["` + tryAsk + `"]}`, "try-" + strconv.FormatInt(at("11:58").UnixNano(), 10): `{"id":"try-2","repository":"novox/mesh-delivery","head":"h","trunk":"t","state":"running","started":"2026-10-26T11:58:00Z"}`, }), Backlog: func() ([]BacklogRow, error) { return []BacklogRow{{Language: "go", Kind: "check", Waiting: 3, Held: 2, OldestSecs: 240}}, nil }, } } func seatOf(t *testing.T, a UsageAnswer, node, seat string) SeatUsage { t.Helper() for _, n := range a.Nodes { for _, s := range n.Seats { if n.Node == node && s.Seat == seat { return s } } } t.Fatalf("no seat %s on %s in %+v", seat, node, a.Nodes) return SeatUsage{} } // build_usage answers, from the states alone, what the operator asked in six calls: per node its seats, capacity, // eligibility, the asks running by kind and the ones left running, the last hour and day, and mesh-wide the backlog. func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) { a := usageFixture().Usage() if testing.Verbose() { out, _ := json.MarshalIndent(a, "", " ") t.Logf("build_usage answers:\n%s", out) } sg := seatOf(t, a, "shanks", "node-build-go") eligible := map[string]string{} for _, n := range a.Nodes { eligible[n.Node] = n.Eligible } for _, c := range []struct { what string got, want any }{ {"nodes, novox named by the default capacity", len(a.Nodes), 4}, {"eligible", eligible, map[string]string{"shanks": "eligible", "ace": "not eligible", "g14": "not eligible", "novox": "not eligible"}}, {"a resolved key with no readable value: the puller uses 0", seatOf(t, a, "g14", "node-build-node").WhyNot, "its capacity on g14 cannot be read, so its puller uses 0"}, {"and is said so", seatOf(t, a, "g14", "node-build-node").CapacityFrom, "mesh-config's value for mesh-build-node on g14 cannot be read (mesh-config resolves it as unreadable), so its puller uses 0"}, {"novox why not", seatOf(t, a, "novox", "node-build-go").WhyNot, "mesh-build-go is not on novox"}, {"novox capacity, from the module's value resolved elsewhere", seatOf(t, a, "novox", "node-build-go").CapacityFrom, "mesh-config, resolved for mesh-build-go on ace (mesh-build-go is not assigned to novox)"}, {"ended by restart or bound, apart", len(sg.EndedStale), 2}, {"ace why not", seatOf(t, a, "ace", "node-build-go").WhyNot, "paused (node-build-go.resume takes asks again)"}, {"g14 why not", seatOf(t, a, "g14", "node-build-go").WhyNot, "its capacity on g14 is 0"}, {"shanks capacity", sg.Capacity, 2}, {"shanks node seat takes the default", seatOf(t, a, "shanks", "node-build-node").Capacity, 3}, {"running", sg.Running.Total, 2}, {"running by kind", sg.Running.ByKind, map[string]int{"delivery": 1, "check": 1}}, {"running steps, the hand step's ended", len(sg.Steps), 2}, {"left running", len(sg.LeftRunning), 1}, {"hour started", sg.LastHour.Started, 5}, // delivery, check, try, check, hand {"hour finished", sg.LastHour.Finished, 3}, // try 600 s, check 360 s, hand 60 s {"hour failed", sg.LastHour.Failed, 1}, {"hour total", sg.LastHour.TotalSecs, int64(1020)}, {"hour median", sg.LastHour.MedianSecs, int64(360)}, {"hour try", sg.LastHour.ByKind["try"], KindWindow{Finished: 1, TotalSecs: 600, MedianSecs: 600}}, {"hour hand", sg.LastHour.ByKind["hand"], KindWindow{Finished: 1, TotalSecs: 60, MedianSecs: 60}}, {"hour at capacity", sg.LastHour.AtCapacitySecs, int64(300)}, // 11:55-12:00, two asks of two {"hour at capacity share", sg.LastHour.AtCapacityShare, 0.083}, {"day finished", sg.LastDay.Finished, 4}, {"day recheck", sg.LastDay.ByKind["recheck"].Finished, 1}, {"day at capacity", sg.LastDay.AtCapacitySecs, int64(300)}, {"backlog", a.Backlog, []BacklogRow{{Language: "go", Kind: "check", Waiting: 3, Held: 2, OldestSecs: 240}}}, {"tries running", len(a.TriesRunning), 1}, {"said", a.Nodes[0].Said[:1], []string{"node-build-go: not eligible, paused (node-build-go.resume takes asks again); 0 of capacity 2 running; last hour 0 finished, median 0 s, at capacity 0% of it; last day 0 finished, median 0 s, at capacity 0.0% of it"}}, {"said shanks", a.Nodes[3].Said[0], "node-build-go: eligible; 2 of capacity 2 running (check 1, delivery 1), 1 left running; last hour 3 finished, median 360 s, at capacity 8% of it; last day 4 finished, median 360 s, at capacity 0.3% of it"}, {"cpu says where it is", strings.Contains(a.Nodes[0].CPU, "/disk-load.cpu_top"), true}, } { if fmt.Sprint(c.got) != fmt.Sprint(c.want) { t.Errorf("%s = %v, want %v", c.what, c.got, c.want) } } if !strings.Contains(sg.LeftRunning[0].Why, "past the step bound") { t.Errorf("left running says %q", sg.LeftRunning[0].Why) } // Nothing readable: each state is named in a note, the backlog's refusal too, and nothing is said held. none := UsageReader{Now: func() time.Time { return usageNow }, Backlog: func() ([]BacklogRow, error) { return nil, errors.New("no socket") }}.Usage() notes := strings.Join(none.Notes, " | ") for _, want := range []string{"have is not readable", "capacity unknown to this tool: mesh-config's resolved values are not readable", "steps of mesh-build-go are not readable", "tries are not readable", "did not answer: no socket"} { if !strings.Contains(notes, want) { t.Errorf("the notes %q do not say %q", notes, want) } } if none.Backlog == nil { t.Errorf("nothing readable answered no backlog list") } // mesh-config unreadable to this tool: a seat that holds its module is not answered (never said not eligible), and its // node's verdict is not known; a seat known not eligible on other grounds (paused) stays so. u := usageFixture() u.Resolved = StateRead{} blind := u.Usage() sb := seatOf(t, blind, "shanks", "node-build-go") verdict := map[string]string{} for _, n := range blind.Nodes { verdict[n.Node] = n.Eligible } if sb.EligibleKnown || sb.Eligible != nil || !strings.HasPrefix(sb.WhyNot, "not known: ") || !strings.HasPrefix(sb.CapacityFrom, "capacity unknown to this tool: ") { t.Errorf("unknown capacity: known %v eligible %v why %q from %q", sb.EligibleKnown, sb.Eligible, sb.WhyNot, sb.CapacityFrom) } if verdict["shanks"] != "not known" || verdict["ace"] != "not eligible" { t.Errorf("verdicts with mesh-config unreadable = %v, want shanks not known, ace not eligible (paused)", verdict) } // Issue 1806's follow-up: the module on shanks says it judges by 1 of mesh-config's 2 (its build CPU bounds it); the // seat says the module's capacity and why. And issue 1890: a running try that names its asks is said with them. u = usageFixture() u.Pauses["go"] = states(map[string]string{"capacity.shanks": `{"v":1,"effective":1,"configured":2,"quota-from":"the cpu.max of /meshyield.slice","quota-cores":6,"cores-per-step":6,"by-cpu":1,"why":"1, bounded by the CPU","at":"2026-10-26T11:00:00Z"}`, "capacity.ace": `{"v":1,"effective":2,"configured":3,"why":"stale: mesh-config says 2 now"}`}) u.Tries = states(map[string]string{"try-" + strconv.FormatInt(at("11:58").UnixNano(), 10): `{"id":"try-2","repository":"novox/mesh-delivery","head":"h","trunk":"t","state":"running","asks":["` + tryAsk0 + `"],"steps":[{"ask":"` + tryAsk0 + `","node":"shanks","state":"running","phase":"unit: tests of 3 packages"}]}`}) bound := u.Usage() sc := seatOf(t, bound, "shanks", "node-build-go") if sc.Capacity != 1 || sc.Configured != 2 || !strings.HasPrefix(sc.CapacityFrom, "1, bounded by the CPU, as mesh-build-go on shanks judges it") { t.Errorf("a seat its CPU bounds: capacity %d of %d, from %q", sc.Capacity, sc.Configured, sc.CapacityFrom) } if sa := seatOf(t, bound, "ace", "node-build-go"); sa.Capacity != 2 || sa.Configured != 0 { t.Errorf("a module's word older than mesh-config's value is not taken: %+v", sa) } // The operator, 2026-10-26: outside its build hours the seat takes nothing new, and says so. u.Pauses["go"] = states(map[string]string{"capacity.shanks": `{"v":1,"effective":0,"configured":2,"build-hours":"18:00-06:00 CET","outside-hours":true,"why":"0, outside its build hours 18:00-06:00 CET until 18:00 (otherwise 2)","at":"2026-10-26T11:00:00Z"}`}) if so := seatOf(t, u.Usage(), "shanks", "node-build-go"); so.Capacity != 0 || !so.OutsideHours || so.Eligible == nil || *so.Eligible || so.WhyNot != "outside its build hours 18:00-06:00 CET on shanks: it takes nothing new until they open" { t.Errorf("a seat outside its build hours: %+v", so) } // A build-hours value the module cannot read: it builds at any hour, and the seat says the value and why. u.Pauses["go"] = states(map[string]string{"capacity.shanks": `{"v":1,"effective":2,"configured":2,"build-hours-fault":"the build hours \"shanks=18:00-6\": \"6\" is not a time HH:MM","why":"2; at any hour, because its build hours cannot be read","at":"2026-10-26T11:00:00Z"}`}) if sf := seatOf(t, u.Usage(), "shanks", "node-build-go"); sf.BuildHours != "any hour" || !strings.Contains(sf.BuildHoursFault, "shanks=18:00-6") || sf.Eligible == nil || !*sf.Eligible { t.Errorf("a seat whose build hours cannot be read: %+v", sf) } if len(bound.TriesRunning) != 1 || len(bound.TriesRunning[0].Asks) != 1 || !strings.Contains(string(bound.TriesRunning[0].Steps), "unit: tests") || strings.Contains(strings.Join(bound.Notes, " | "), "name no ask yet") { t.Errorf("a running try naming its asks: %+v, notes %v", bound.TriesRunning, bound.Notes) } } var tryAsk0 = stepID(at("11:58"), "a00010")