Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go
T
jschoubben 76e4ec4dc6
mesh/delivery merged: source.main moved to novox/mesh-catalog@5a4ac4991395
mesh/delivery-group group feat/1781-milestone-records merged: every member is merged
mesh/check checking 76e4ec4dc680 on 26908a89506f | novox/mesh-catalog#506 in group feat/1781-milestone-records
mesh-issues and mesh-knowledge: milestones are records of the issue tracker, and progress reads them (ADR 0493 §1 to §4, issue 1781)
mesh-issues keeps each design's milestone in the state milestones (history 5), says milestone-set and
milestone-moved, and serves milestones, milestone-set and milestone-move. An issue's milestone is its own link
of kind milestone, so at most one milestone per issue is checked inside the issue's own write; kind commit
links a fixing commit. The triage links no milestone, pull request or commit. import with milestones=N seeds
a design's table once. mesh-knowledge's progress reads issue-tracker.milestones and its table parse is gone.

Squashed from four commits so the branch history does not carry a compiled bundle that was committed by
mistake (6 MB); the path is now in .gitignore.
2026-10-26 18:42:51 +01:00

385 lines
14 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"testing"
"time"
)
// The progress verb's tests (novox/hq issue 1947). The designs are hq's, captured at a commit; the tracker's rows and
// the delivery seat's answers were captured from the live mesh on 2026-10-26 (testdata/progress/README.md).
func progressRecords() dirRecords {
return dirRecords{dir: filepath.Join("testdata", "progress", "hq")}
}
// withDesigns is the fixture's records with the captured designs beside them, as the bus test's records module
// serves them: a folder or document the first does not hold is read from the second.
type withDesigns struct{ base, designs dirRecords }
func (w withDesigns) Standing() (string, error) { return w.base.Standing() }
func (w withDesigns) List(folder string) ([]string, []string, error) {
if f, d, err := w.base.List(folder); err == nil {
return f, d, nil
}
return w.designs.List(folder)
}
func (w withDesigns) Read(path string) (string, string, error) {
if c, commit, err := w.base.Read(path); err == nil {
return c, commit, nil
}
c, _, err := w.designs.Read(path)
commit, _ := w.base.Standing()
return c, commit, err
}
// seatsWorld plays the issue tracker and mesh-delivery from the captured answers, and counts what it is asked.
type seatsWorld struct {
mu sync.Mutex
rows map[int]json.RawMessage
linked map[string][]int
// milestones is the tracker's milestone records (issue 1781).
milestones json.RawMessage
asked map[string]int
files map[string]json.RawMessage
}
func newSeatsWorld(t *testing.T) *seatsWorld {
t.Helper()
s := &seatsWorld{rows: map[int]json.RawMessage{}, linked: map[string][]int{}, asked: map[string]int{}, files: map[string]json.RawMessage{}}
var rows []json.RawMessage
if err := json.Unmarshal(readFixture(t, "tracker-rows.json"), &rows); err != nil {
t.Fatal(err)
}
for _, r := range rows {
var n struct{ Number int }
_ = json.Unmarshal(r, &n)
s.rows[n.Number] = r
}
s.milestones = readFixture(t, "tracker-milestones-47.json")
for _, f := range []string{"deliveries.json", "groups.json", "show-car-hunter-2.json"} {
s.files[f] = readFixture(t, f)
}
return s
}
func readFixture(t *testing.T, name string) json.RawMessage {
t.Helper()
raw, err := os.ReadFile(filepath.Join("testdata", "progress", name))
if err != nil {
t.Fatal(err)
}
return raw
}
func (s *seatsWorld) ask(key string, body any) (json.RawMessage, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.asked[key]++
raw, _ := json.Marshal(body)
var a struct {
Numbers []int `json:"numbers"`
Linked string `json:"linked"`
Number int `json:"number"`
ID string `json:"id"`
Design int `json:"design"`
}
_ = json.Unmarshal(raw, &a)
switch key {
case MilestonesVerb:
if a.Design != 0 && a.Design != 47 {
return json.RawMessage(`{"milestones": []}`), nil
}
return s.milestones, nil
case "seat:issue-tracker.list":
numbers := a.Numbers
if a.Linked != "" {
numbers = s.linked[a.Linked]
}
var rows []json.RawMessage
for _, n := range numbers {
if r, ok := s.rows[n]; ok {
rows = append(rows, r)
}
}
return json.Marshal(map[string]any{"rows": rows, "matched": len(rows)})
case "seat:issue-tracker.get":
return json.Marshal(map[string]any{"number": a.Number, "notes": []map[string]any{{"at": "2026-10-26T15:12:15Z", "by": "shanks.node-tools", "text": "a note"}},
"notes-left-out": 96, "links": []map[string]any{{"kind": "depends-on", "target": "1943"}}})
case "seat:mesh-delivery.deliveries":
return s.files["deliveries.json"], nil
case "seat:mesh-delivery.groups":
return s.files["groups.json"], nil
case "seat:mesh-delivery.show":
if a.ID == "jschoubben/car-hunter@cc4c8ce12901" {
return s.files["show-car-hunter-2.json"], nil
}
return json.RawMessage(`{"delivery": {}}`), nil
}
return nil, os.ErrNotExist
}
// countingRecords is Records that counts the documents read and can be moved to another commit.
type countingRecords struct {
dirRecords
commit string
reads int
}
func (c *countingRecords) Standing() (string, error) { return c.commit, nil }
func (c *countingRecords) Read(p string) (string, string, error) {
c.reads++
content, _, err := c.dirRecords.Read(p)
return content, c.commit, err
}
// 1947/AC2, as issue 1781 changed it: a design's title, state and decisions are hq's; its milestones are the issue
// tracker's records, ordered as its table, an id found with spaces or hyphens, and a closed one holding no issue done.
func TestADesignsMilestonesAreTheTrackersRecords(t *testing.T) {
content, _, err := progressRecords().Read(DesignsFolder + "/47-delivery-from-commit-to-delivered.md")
if err != nil {
t.Fatal(err)
}
d := parseDesign(DesignsFolder+"/47-delivery-from-commit-to-delivered.md", content)
if d.Number != 47 || d.Title != "Delivery: from a commit to delivered" || d.Status != "in-progress" || !contains(d.Decisions, "ADR 0276") ||
len(d.Milestones) != 0 {
t.Fatalf("design %d %q %q %v, %d milestones read from hq", d.Number, d.Title, d.Status, d.Decisions, len(d.Milestones))
}
seats := newSeatsWorld(t)
k := &KB{Seats: seats.ask}
ds, err := k.withMilestones([]*Design{d}, 47)
if err != nil {
t.Fatal(err)
}
if len(ds[0].Milestones) != 29 || len(d.Milestones) != 0 || seats.asked[MilestonesVerb] != 1 {
t.Fatalf("%d milestones; the cached design holds %d", len(ds[0].Milestones), len(d.Milestones))
}
for _, c := range []struct {
id string
want []int
}{
{"1", []int{973, 525}},
{"R6", []int{1942, 1945, 1943, 1944, 1946, 1906, 1130}},
{"r4d", []int{}},
{"3 fix", []int{1082, 1049, 1047, 1083, 1079, 1084}},
} {
m := ds[0].milestone(c.id)
if m == nil {
t.Fatalf("no milestone %s", c.id)
}
if !reflect.DeepEqual(m.Issues, c.want) {
t.Errorf("milestone %s: %v, want %v", c.id, m.Issues, c.want)
}
}
closed := &Design{Milestones: []Milestone{{ID: "6", Status: "closed", Reason: "folded into release 4"}, {ID: "R4d", Status: "open"}}}
items, _ := milestoneItems(closed, nil, nil)
if items[0].State != StateDone || items[1].State != StateWaiting {
t.Fatalf("%+v", items)
}
}
func containsInt(xs []int, n int) bool {
for _, x := range xs {
if x == n {
return true
}
}
return false
}
// 1947/AC6: the designs are read once per commit of hq, and again when it moves; the mesh level names those with
// milestones one by one and counts the rest.
func TestDesignsAreReadAgainOnlyWhenHqMoves(t *testing.T) {
rec := &countingRecords{dirRecords: progressRecords(), commit: "aaaa"}
seats := newSeatsWorld(t)
k := &KB{Records: rec, Seats: seats.ask, Now: func() time.Time { return time.Date(2026, 10, 26, 15, 30, 0, 0, time.UTC) }}
p, err := k.Progress(ProgressAsk{Of: "mesh"})
if err != nil {
t.Fatal(err)
}
if rec.reads != 3 {
t.Fatalf("read %d documents for the mesh, want 3", rec.reads)
}
if !strings.Contains(p.Tree, "3 designs in progress, 1 with milestones") || !strings.Contains(p.Tree, "design 47 · Delivery: from a commit to delivered") ||
!strings.Contains(p.Tree, "2 designs in progress name no milestones (issue 845): 53, 57") {
t.Fatalf("mesh:\n%s", p.Tree)
}
for range 2 {
if _, err := k.Progress(ProgressAsk{Of: "design 47"}); err != nil {
t.Fatal(err)
}
}
if rec.reads != 3 {
t.Fatalf("read %d documents with hq standing still, want 3", rec.reads)
}
rec.commit = "bbbb"
if _, err := k.Progress(ProgressAsk{Of: "design 47"}); err != nil {
t.Fatal(err)
}
if rec.reads != 4 {
t.Fatalf("read %d documents after hq moved, want 4", rec.reads)
}
if _, err := k.Progress(ProgressAsk{Of: "design 99"}); err == nil || !strings.Contains(err.Error(), "no design 99") {
t.Fatalf("an unknown design: %v", err)
}
}
// 1947/AC3: an issue's state from its tracker row, and a parent's from its children.
func TestIssueStatesAndTheirRollUp(t *testing.T) {
seats := newSeatsWorld(t)
row := func(n int) trackerRow {
var r trackerRow
if err := json.Unmarshal(seats.rows[n], &r); err != nil {
t.Fatal(err)
}
return r
}
for _, c := range []struct {
n int
state string
why string
counts bool
}{
{1942, StateDone, "resolved, delivered", true},
{1943, StateDone, "resolved, nothing to deliver", true},
{1946, StateMoving, "resolved · fix merged, not delivered: mesh-catalog#475", true},
{1906, StateStuck, "resolved · held: mesh-delivery#39", true},
{1945, StateMoving, "fixing since", true},
{1049, StateStuck, "worker fix-chain-cat@shanks stale since", true},
{1126, StateWaiting, "open, needs a decision", true},
{774, StateWaiting, "open, ready", true},
{1021, "", "", false},
} {
it, counts := issueItem(row(c.n))
if counts != c.counts || it.State != c.state || !strings.Contains(it.Why, c.why) {
t.Errorf("issue %d: %s %q (counts %v), want %s %q", c.n, it.State, it.Why, counts, c.state, c.why)
}
}
// The other delivery lines a resolved issue carries on the live tracker (981, 1290 and 1235 on 2026-10-26).
for line, state := range map[string]string{
"fix not delivered: mesh-catalog#357 stopped:": StateStuck,
"fix not delivered: mesh-catalog#389 failed:": StateStuck,
"nothing heard from the delivery of mesh-delivery#5": StateWaiting,
} {
if it, _ := issueItem(trackerRow{Number: 1, Status: "resolved", Delivery: line}); it.State != state {
t.Errorf("resolved with %q: %s, want %s", line, it.State, state)
}
}
at := func(h int) *time.Time { t := time.Date(2026, 10, 26, h, 0, 0, 0, time.UTC); return &t }
for _, c := range []struct {
name string
kids []Item
state string
done int
moved *time.Time
}{
{"all done", []Item{{State: StateDone, Moved: at(9)}, {State: StateDone, Moved: at(11)}}, StateDone, 2, at(11)},
{"one moves", []Item{{State: StateDone}, {State: StateMoving, Moved: at(12)}, {State: StateWaiting}}, StateMoving, 1, at(12)},
{"one stuck outranks moving", []Item{{State: StateMoving}, {State: StateStuck}}, StateStuck, 0, nil},
{"only waiting", []Item{{State: StateDone}, {State: StateWaiting}}, StateWaiting, 1, nil},
{"no issue yet", nil, StateWaiting, 0, nil},
} {
var p Item
rollUp(&p, c.kids)
if p.State != c.state || p.Done != c.done || !reflect.DeepEqual(p.Moved, c.moved) {
t.Errorf("%s: %s %d %v", c.name, p.State, p.Done, p.Moved)
}
}
}
// 1947/AC1 (its unit side): design 47 in its two forms, and one milestone.
func TestProgressOfADesignMinimalAndDetailed(t *testing.T) {
seats := newSeatsWorld(t)
k := &KB{Records: progressRecords(), Seats: seats.ask, Now: func() time.Time { return time.Date(2026, 10, 26, 15, 30, 0, 0, time.UTC) }}
p, err := k.Progress(ProgressAsk{Of: "design 47"})
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(p)
if len(raw) > DefaultBound || p.Detail != DetailMinimal || !strings.Contains(p.Source, "the issue tracker's milestone records of design 47") {
t.Fatalf("%d bytes, %s, %s", len(raw), p.Detail, p.Source)
}
if seats.asked["seat:issue-tracker.list"] != 1 || seats.asked["seat:mesh-delivery.deliveries"] != 0 || seats.asked["seat:issue-tracker.get"] != 0 {
t.Fatalf("minimal asked %v", seats.asked)
}
if w := strings.Join(p.Warnings, "\n"); !strings.Contains(w, "the tracker neither answered nor named missing") || !strings.Contains(w, "973, 525") {
t.Errorf("a row the tracker did not answer is not said: %v", p.Warnings)
}
lines := strings.Split(p.Tree, "\n")
if !strings.HasPrefix(lines[0], "✗ design 47 · Delivery: from a commit to delivered — ") {
t.Fatalf("first line %q", lines[0])
}
for _, want := range []string{"✗ R6 4/7 1945 fixing since", "✗ 3-fix 0/1 1049 fixing, worker fix-chain-cat@shanks stale since"} {
if !strings.Contains(p.Tree, want) {
t.Errorf("no %q in\n%s", want, p.Tree)
}
}
d, err := k.Progress(ProgressAsk{Of: "design 47", Detail: DetailDetailed, Bound: MostBound})
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"├── ▶ 1946 ", "resolved · fix merged, not delivered: mesh-catalog#475",
"▶ 1945 The controller still plans", "jschoubben/car-hunter@cc4c8ce12901 ready since 10-26 17:05, waits for its merge",
"depends on 1943", "97 notes; last 10-26 17:12 by shanks.node-tools"} {
if !strings.Contains(d.Tree, want) {
t.Errorf("detailed: no %q in\n%s", want, d.Tree)
}
}
if len(d.Items) != 1 || d.Items[0].ID != "design 47" {
t.Fatalf("items %+v", d.Items)
}
m, err := k.Progress(ProgressAsk{Of: "milestone 47/r6", Detail: DetailDetailed})
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(m.Tree, "✗ milestone 47/R6 · ") || !strings.Contains(m.Tree, "✓ 1130") {
t.Fatalf("milestone:\n%s", m.Tree)
}
small, _ := k.Progress(ProgressAsk{Of: "design 47", Detail: DetailDetailed, Bound: LeastBound})
if raw, _ := json.Marshal(small); len(raw) > LeastBound || !strings.Contains(small.Cut, "raise bound") {
t.Fatalf("%d bytes within %d; cut %q", len(raw), LeastBound, small.Cut)
}
for _, bad := range []ProgressAsk{{Of: "design"}, {Of: "milestone 47/R99"}, {Of: "design 47", Detail: "full"}, {Of: "design 57"}} {
if _, err := k.Progress(bad); err == nil {
t.Errorf("%+v was answered", bad)
}
}
}
// 1947/AC5: an issue's own level, and a record's: the designs that name it and the issues linked to it.
func TestProgressOfAnIssueAndARecord(t *testing.T) {
w := imported(t, newMemStore())
seats := newSeatsWorld(t)
seats.linked["decision:0276"] = []int{1942, 1945}
w.k.Records, w.k.Seats = progressRecords(), seats.ask
p, err := w.k.Progress(ProgressAsk{Of: "issue 1945"})
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"▶ issue 1945 · The controller still plans", "97 notes", "depends on 1943",
"jschoubben/car-hunter@cc4c8ce12901 ready"} {
if !strings.Contains(p.Tree, want) {
t.Errorf("issue: no %q in\n%s", want, p.Tree)
}
}
r, err := w.k.Progress(ProgressAsk{Of: "ADR 0276", Detail: DetailDetailed})
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"ADR 0276 · ", "named by design 47 (in-progress)", "▶ 1 of 2 linked issues done", "✓ 1942"} {
if !strings.Contains(r.Tree, want) {
t.Errorf("record: no %q in\n%s", want, r.Tree)
}
}
if _, err := w.k.Progress(ProgressAsk{Of: "issue 99999"}); err == nil {
t.Error("an issue the tracker does not hold was answered")
}
}