mesh/delivery merged: source.main moved to novox/mesh-catalog@26908a89506f
mesh/delivery-group group feat/1947-progress merged: every member is merged
mesh/check pass 87fe2c45cfed on 26908a89506f | carried from 27a7757b970e, hq 3a5c4c7c4d9c | unit 2091 run, 2327 kept, 6 skipped | integration 2112 run…
A resolved issue whose delivery is held or failed is stuck, one merged and not delivered is moving; a row the tracker did not answer, an answer that does not read and linked issues cut by the list's limit are said as warnings instead of passing silently.
385 lines
14 KiB
Go
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
|
|
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
|
|
}
|
|
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"`
|
|
}
|
|
_ = json.Unmarshal(raw, &a)
|
|
switch key {
|
|
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: the milestone table of design 47, as hq holds it, read into milestones with their issues, and what the
|
|
// prose does not say for certain said as warnings.
|
|
func TestTheDesignReaderReadsTheMilestoneTable(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") {
|
|
t.Fatalf("design %d %q %q %v", d.Number, d.Title, d.Status, d.Decisions)
|
|
}
|
|
if len(d.Milestones) != 29 {
|
|
t.Fatalf("%d milestones", len(d.Milestones))
|
|
}
|
|
for _, c := range []struct {
|
|
id string
|
|
want []int
|
|
}{
|
|
{"1", []int{973, 525}},
|
|
{"2b", []int{1025, 1019}},
|
|
{"R6", []int{1942, 1945, 1943, 1944, 1946, 1906, 1130}},
|
|
{"r4d", []int{}},
|
|
{"3 fix", []int{1082, 1049, 1047, 1083, 1079, 1084}},
|
|
} {
|
|
m := d.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)
|
|
}
|
|
}
|
|
if r4a := d.milestone("R4a"); len(r4a.Issues) != 14 || !containsInt(r4a.Issues, 1360) {
|
|
t.Errorf("R4a's range 1353 to 1364 read as %v", r4a.Issues)
|
|
}
|
|
warned := strings.Join(d.Warnings, "\n")
|
|
for _, w := range []string{"issue 1209 is in milestones R3b and R3c", "issue 971 is in milestones 6 and R4c",
|
|
`"1353 to 1364" is a range in words, read as the 12 issues`, "milestone R4d names no issue"} {
|
|
if !strings.Contains(warned, w) {
|
|
t.Errorf("no warning %q in\n%s", w, warned)
|
|
}
|
|
}
|
|
// Design 57's range with an exception, and a design with no milestone table.
|
|
content, _, _ = progressRecords().Read(DesignsFolder + "/57-building-config-and-provisioning.md")
|
|
d57 := parseDesign(DesignsFolder+"/57-building-config-and-provisioning.md", content)
|
|
if m := d57.milestone("1"); m == nil || containsInt(m.Issues, 659) || !containsInt(m.Issues, 678) || !containsInt(m.Issues, 685) {
|
|
t.Errorf("design 57 milestone 1: %+v", d57.Milestones)
|
|
}
|
|
content, _, _ = progressRecords().Read(DesignsFolder + "/53-the-issue-tracker.md")
|
|
if d53 := parseDesign(DesignsFolder+"/53-the-issue-tracker.md", content); len(d53.Milestones) != 0 || d53.Status != "in-progress" {
|
|
t.Errorf("design 53: %+v", d53.Milestones)
|
|
}
|
|
}
|
|
|
|
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, 2 with milestones") || !strings.Contains(p.Tree, "design 47 · Delivery: from a commit to delivered") ||
|
|
!strings.Contains(p.Tree, "1 designs in progress name no milestones (issue 845): 53") {
|
|
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, "issue 1781") {
|
|
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"}} {
|
|
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")
|
|
}
|
|
}
|