Knowledge base: progress says how far a design, milestone, issue or record has come (issue 1947)
A lead's five-minute report and 'is design 47 nearing its end' were joined by hand from the design file, the tracker and the deliveries. One verb now works the state out (ADR 0424 §1), minimal for every five minutes, detailed for the report's tree. Milestones are read from the design's table until the tracker's milestone records (issue 1781) are delivered.
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
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")}
|
||||
}
|
||||
|
||||
// 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", true},
|
||||
{1946, StateDone, "resolved · fix merged, not delivered: mesh-catalog#475", true},
|
||||
{1906, StateDone, "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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 6/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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user