package main import ( "encoding/json" "errors" "fmt" "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) } } // 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 p.Cut != "" || len(p.Items) != 0 { t.Errorf("a minimal answer leaves the items to its tree and is not a cut: cut %q, %d items", p.Cut, len(p.Items)) } warns := []string{"mesh-delivery.show a answered something else: x", "issue-tracker.get 1: refused", "the tracker did not name 2", "issue-tracker.get 3 answered something else: y"} callsFirst(warns) if strings.Join(warns, "|") != "issue-tracker.get 1: refused|the tracker did not name 2|mesh-delivery.show a answered something else: x|issue-tracker.get 3 answered something else: y" { t.Errorf("call failures do not come first: %q", warns) } 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) } if big, _ := k.Progress(ProgressAsk{Of: "design 47", Bound: 8192}); big.Cut != "" || big.Tree != p.Tree { t.Errorf("a minimal answer at bound 8192 is cut: %q", big.Cut) } 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") } } // sinceWorld plays the tracker and the delivery seat from what they answered on 2026-10-28 (testdata/progress/since): // a get or a show it holds no answer for is refused, as a seat that cannot answer. The tracker's changes answers what // mesh-issues' verb made of 1422's open and move to fixing (testdata/progress/README.md); without changes it is // refused, as by a tracker that has no such verb yet. func sinceWorld(t *testing.T, changes bool) func(string, any) (json.RawMessage, error) { t.Helper() read := func(name string) json.RawMessage { return readFixture(t, filepath.Join("since", name)) } var rows []json.RawMessage if err := json.Unmarshal(read("rows.json"), &rows); err != nil { t.Fatal(err) } byNumber := map[int]json.RawMessage{} for _, r := range rows { var n struct{ Number int } _ = json.Unmarshal(r, &n) byNumber[n.Number] = r } shows := map[string]string{"novox/mesh-catalog@470687ea4cce": "show-mesh-catalog-539.json", "novox/mesh-catalog@f5d4b2f86ed4": "show-mesh-catalog-537.json", "novox/mesh-delivery@2b801f5bd49a": "show-mesh-delivery-94.json", "novox/de-spiegel@fba6dbe3259e": "show-de-spiegel-6.json"} return func(key string, body any) (json.RawMessage, error) { raw, _ := json.Marshal(body) var a struct { Numbers []int `json:"numbers"` Number int `json:"number"` ID string `json:"id"` One string `json:"one"` } _ = json.Unmarshal(raw, &a) switch key { case MilestonesVerb: return read("milestone-47-6.json"), nil case "seat:issue-tracker.list": var out []json.RawMessage for _, n := range a.Numbers { if r, ok := byNumber[n]; ok { out = append(out, r) } } return json.Marshal(map[string]any{"rows": out, "matched": len(out)}) case "seat:issue-tracker.changes": if changes { return read("changes.json"), nil } return nil, errors.New("mesh-issues has no tool issue-tracker.changes") case "seat:issue-tracker.get": if a.Number == 1422 || a.Number == 1956 { return read(fmt.Sprintf("get-%d.json", a.Number)), nil } case "seat:mesh-delivery.deliveries": return read("deliveries.json"), nil case "seat:mesh-delivery.groups": return json.RawMessage(`[]`), nil case "seat:mesh-delivery.show": if f, ok := shows[a.ID]; ok { return read(f), nil } } return nil, os.ErrNotExist } } // The operator, 2026-10-28: "can the progress tool also report the progress made since the last call, or maybe better, // the last 60 minutes". With since, an answer lists only what changed in that window under of, read from the // tracker's notes, links, workers and closes, its milestones' history and mesh-delivery's transitions and checks. func TestProgressSinceAWindowSaysOnlyWhatChanged(t *testing.T) { at := func(s string) *time.Time { v, _ := time.Parse(time.RFC3339, s); return &v } k := &KB{Records: progressRecords(), Seats: sinceWorld(t, true), Now: func() time.Time { return *at("2026-10-28T11:13:30Z") }} p, err := k.Progress(ProgressAsk{Of: "issue 1422", Since: "2h"}) if err != nil { t.Fatal(err) } lines := strings.Split(p.Tree, "\n") if lines[0] != "17 changes in the last 120 min under issue 1422 (since "+when(at("2026-10-28T09:13:30Z"))+")" || p.Changes != 17 || p.Since != "2h0m0s" { t.Fatalf("%d changes, since %q:\n%s", p.Changes, p.Since, p.Tree) } for _, want := range []string{ "▶ 1422 " + clip("The catch-up reads the trunk 'main' of repositories the catalogue follows by another branch or by a commit, and the forge holds none", 80) + " — open → fixing by fix-trunk-unknown@shanks " + when(at("2026-10-28T09:38:37Z")) + "; 9 notes, the last " + when(at("2026-10-28T11:05:21Z")) + " by shanks.node-tools", "✓ mesh-catalog#537 (1422) opened " + when(at("2026-10-28T10:20:56Z")) + " · check pass " + when(at("2026-10-28T10:30:45Z")) + " · merged " + when(at("2026-10-28T10:34:49Z")) + ", on the machines", "✓ mesh-delivery#94 (1126, 1422) opened ", "▶ mesh-catalog#539 (1422, 1956) opened " + when(at("2026-10-28T11:07:42Z")) + ", now ready, waits for its merge", } { if !strings.Contains(p.Tree, want) { t.Errorf("no %q in\n%s", want, p.Tree) } } if strings.Contains(p.Tree, "de-spiegel") || strings.Contains(p.Tree, "triage") { t.Errorf("a change outside issue 1422 or the window is said:\n%s", p.Tree) } w := strings.Join(p.Warnings, "\n") if !strings.Contains(w, "keeps no time a merge reached its machines") || strings.Contains(w, "issue-tracker.changes") { t.Errorf("the warnings: %v", p.Warnings) } // A tracker that cannot say its moves: each is said by the status it reached, and that is said. old := &KB{Records: progressRecords(), Seats: sinceWorld(t, false), Now: k.Now} o, err := old.Progress(ProgressAsk{Of: "issue 1422", Since: "2h"}) if err != nil { t.Fatal(err) } if !strings.Contains(o.Tree, " — → fixing by fix-trunk-unknown@shanks "+when(at("2026-10-28T09:38:37Z"))) || o.Changes != 17 || !strings.Contains(strings.Join(o.Warnings, "\n"), "issue-tracker.changes: mesh-issues has no tool issue-tracker.changes; a move is said by the status it reached") { t.Errorf("without changes:\n%s\n%v", o.Tree, o.Warnings) } d, err := k.Progress(ProgressAsk{Of: "issue 1422", Since: "2h", Detail: DetailDetailed}) if err != nil { t.Fatal(err) } for _, want := range []string{"│ ├── open → fixing " + when(at("2026-10-28T09:38:37Z")) + " by shanks.node-tools, worker fix-trunk-unknown@shanks", "│ ├── noted " + when(at("2026-10-28T09:38:09Z")) + " by shanks.node-tools: lead-47@shanks 2026-10-28 09:40Z.", " └── merged " + when(at("2026-10-28T10:34:49Z")) + ": on the trunk its modules follow: merged there"} { if !strings.Contains(d.Tree, want) { t.Errorf("detailed: no %q in\n%s", want, d.Tree) } } // A milestone's issue the tracker's get cannot answer falls back to its row's moved time, said as such. m, err := k.Progress(ProgressAsk{Of: "milestone 47/L2", Since: "60m"}) if err != nil { t.Fatal(err) } for _, want := range []string{"6 changes in the last 60 min under milestone 47/L2", "▶ 1126 " + clip("About 49,800 controller code lines stay after the delivery model, against a first limit of 25,000", 80) + " — moved " + when(at("2026-10-28T11:13:28Z")) + ", now fixing (lead-47@shanks)", "✓ de-spiegel#6 (1126) check pass ", "✓ mesh-delivery#94 (1126, 1422) opened "} { if !strings.Contains(m.Tree, want) { t.Errorf("milestone: no %q in\n%s", want, m.Tree) } } if w := strings.Join(m.Warnings, "\n"); !strings.Contains(w, "issue-tracker.get 1126") { t.Errorf("the fallback is not said: %v", m.Warnings) } if small, _ := k.Progress(ProgressAsk{Of: "issue 1422", Since: "2h", Detail: DetailDetailed, Bound: LeastBound}); small == nil || size(small) > LeastBound || !strings.Contains(small.Cut, "raise bound") { t.Errorf("a window's answer is not kept within its bound: %+v", small) } // A milestone's move, from its history: what it was before, what it is now, who moved it and when. k.Now = func() time.Time { return *at("2026-10-26T18:40:00Z") } ds, err := k.Progress(ProgressAsk{Of: "design 47", Since: "60m"}) if err != nil { t.Fatal(err) } if ds.Tree != "1 change in the last 60 min under design 47 (since "+when(at("2026-10-26T17:40:00Z"))+")\n└── ✓ milestone 47/6 open → closed: folded into R4g · by shanks.node-tools "+when(at("2026-10-26T18:10:10Z")) { t.Errorf("design:\n%s", ds.Tree) } for _, bad := range []string{"0m", "-5m", "25h", "a day", "5"} { if _, err := k.Progress(ProgressAsk{Of: "issue 1422", Since: bad}); err == nil || !strings.Contains(err.Error(), "since") { t.Errorf("since %q: %v", bad, err) } } }