Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/milestone_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

188 lines
8.2 KiB
Go

package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
// Issue 1781 (ADR 0493 §1 to §4): a milestone is a record made by its verb; an issue is linked to at most one, only to
// an open one, and only by the verb; the triage links no milestone, pull request or commit.
func TestAMilestoneIsMadeByItsVerbAndAnIssueBelongsToAtMostOne(t *testing.T) {
h := newHarness(t)
a, b := h.open(t, "a"), h.open(t, "b")
for _, c := range []struct {
ask MilestoneSetAsk
says string
}{
{MilestoneSetAsk{Design: 0, ID: "R6", Outcome: "x"}, "design's number"},
{MilestoneSetAsk{Design: 47, ID: "R6/x", Outcome: "x"}, "short name"},
{MilestoneSetAsk{Design: 47, ID: "R6"}, "needs its outcome"},
{MilestoneSetAsk{Design: 47, ID: "R6", Outcome: "nodes apply their wants", Order: 2}, ""},
{MilestoneSetAsk{Design: 47, ID: "3 fix", Outcome: "a group merges in one pass", Order: 1}, ""},
{MilestoneSetAsk{Design: 47, ID: "R6", Outcome: "nodes apply their wants"}, "nothing changed"},
} {
out, err := h.milestoneSet("lead@shanks", c.ask)
if c.says == "" && err != nil || c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)) &&
(out == nil || !strings.Contains(out.Said, c.says)) {
t.Fatalf("%+v: want %q, got %+v %v", c.ask, c.says, out, err)
}
}
links := []struct {
l LinkAsk
says string
}{
{LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/R6"}, ""},
{LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/3 fix"}, "in milestone 47/R6 already"},
{LinkAsk{Number: b, Kind: LinkMilestone, Target: "47/R9"}, "there is no milestone 47/R9"},
{LinkAsk{Number: b, Kind: LinkMilestone, Target: "R6"}, "design/id"},
{LinkAsk{Number: b, Kind: LinkCommit, Target: "mesh-catalog@" + strings.Repeat("a", 40)}, ""},
{LinkAsk{Number: b, Kind: LinkCommit, Target: "mesh-catalog@abc123"}, "full 40-character"},
{LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/R6", Remove: true}, ""},
{LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/3-fix"}, ""},
{LinkAsk{Number: b, Kind: LinkMilestone, Target: "47/R6"}, ""},
}
for _, c := range links {
_, err := h.link("lead@shanks", c.l)
if c.says == "" && err != nil || c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)) {
t.Fatalf("%+v: want %q, got %v", c.l, c.says, err)
}
}
for _, kind := range madeByPeople {
target := map[string]string{LinkPullRequest: "mesh-catalog#1", LinkMilestone: "47/R6", LinkCommit: "mesh-catalog@" + strings.Repeat("b", 40)}[kind]
if _, err := h.link(GroomBy, LinkAsk{Number: a, Kind: kind, Target: target}); err == nil || !strings.Contains(err.Error(), "triage links no") {
t.Fatalf("the triage linked %s: %v", kind, err)
}
}
read, err := h.milestones(MilestonesAsk{Design: 47})
if err != nil {
t.Fatal(err)
}
if len(read.Milestones) != 2 || read.Milestones[0].Ref != "47/3-fix" || read.Milestones[1].Ref != "47/R6" ||
fmt.Sprint(read.Milestones[0].Issues) != fmt.Sprint([]uint64{a}) || fmt.Sprint(read.Milestones[1].Issues) != fmt.Sprint([]uint64{b}) {
t.Fatalf("%+v", read.Milestones)
}
one, err := h.milestones(MilestonesAsk{One: "47/R6"})
if err != nil || len(one.Milestones) != 1 || len(one.History) == 0 || one.Milestones[0].Made.By != "lead@shanks" {
t.Fatalf("%+v %v", one, err)
}
// Closing waits for its issues; a closed milestone takes no new one; each change is said.
if _, err := h.milestoneMove("lead@shanks", "47/R6", MilestoneClosed, "delivered"); err == nil || !strings.Contains(err.Error(), fmt.Sprint(b)) {
t.Fatalf("closed over an open issue: %v", err)
}
if _, err := h.Move(MoveAsk{Number: b, Status: StatusWontfix, Reason: "not needed"}); err != nil {
t.Fatal(err)
}
if _, err := h.milestoneMove("lead@shanks", "47/R6", MilestoneClosed, "delivered"); err != nil {
t.Fatal(err)
}
if _, err := h.link("lead@shanks", LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/R6", Remove: true}); err == nil {
t.Fatal("a removed a link it does not have")
}
c := h.open(t, "c")
if _, err := h.link("lead@shanks", LinkAsk{Number: c, Kind: LinkMilestone, Target: "47/R6"}); err == nil || !strings.Contains(err.Error(), "is closed") {
t.Fatalf("linked to a closed milestone: %v", err)
}
if out, err := h.milestoneSet("lead@shanks", MilestoneSetAsk{Design: 47, ID: "R6", Announce: true}); err != nil || !out.Announced {
t.Fatalf("announce: %+v %v", out, err)
}
var kinds []string
for _, e := range h.said() {
if strings.HasPrefix(e.Event, "milestone") {
kinds = append(kinds, e.Event+":"+fmt.Sprint(e.Body["milestone"]))
}
}
if strings.Join(kinds, " ") != "milestone-set:47/R6 milestone-set:47/3-fix milestone-moved:47/R6 milestone-set:47/R6" {
t.Fatalf("said %v", kinds)
}
}
// listRecords is a record holding one design document.
type listRecords struct {
path, content string
}
func (r listRecords) Standing() (string, error) {
return "0123456789abcdef0123456789abcdef01234567", nil
}
func (r listRecords) List(string) ([]string, []string, error) {
return nil, []string{"12-other.md", filepath.Base(r.path)}, nil
}
func (r listRecords) Read(string) (string, string, error) { return r.content, "", nil }
// Issue 1781: the seed makes each row's milestone, links what one row names, refuses what two rows name or another
// milestone holds, closes a milestone whose issues are done, and run again changes nothing.
func TestTheSeedTakesADesignsTableOnce(t *testing.T) {
h := newHarness(t)
a, b, c, d := h.open(t, "a"), h.open(t, "b"), h.open(t, "c"), h.open(t, "d")
if _, err := h.Move(MoveAsk{Number: d, Status: StatusWontfix, Reason: "folded"}); err != nil {
t.Fatal(err)
}
table := fmt.Sprintf("# 47 — Delivery\n\n## Milestones\n\n| # | Outcome | Issues |\n|---|---|---|\n"+
"| 1 | the first | %d, %d (ADR 0493 §2) |\n| 3 fix | a fix | %d, %d, 999 |\n| R4d | none yet | opened when it is prepared |\n"+
"| 6 | done | %d |\n\n## Next\n\n| a | Issues |\n|---|---|\n| x | %d |\n", a, b, c, b, d, a)
rec := listRecords{path: "03-DESIGN/01-to-be/47-delivery.md", content: table}
dry, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 47, DryRun: true}, rec)
if err != nil {
t.Fatal(err)
}
if dry.Made != 4 || dry.Linked != 3 || dry.Refused != 2 || dry.Closed != 1 || len(dry.Rows[1].Missing) != 1 {
t.Fatalf("dry run %+v", dry)
}
if read, _ := h.milestones(MilestonesAsk{}); len(read.Milestones) != 0 {
t.Fatalf("a dry run wrote %+v", read.Milestones)
}
got, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 47}, rec)
if err != nil {
t.Fatal(err)
}
if fmt.Sprintf("%d %d %d %d", got.Made, got.Linked, got.Refused, got.Closed) != "4 3 2 1" {
t.Fatalf("%+v", got)
}
read, _ := h.milestones(MilestonesAsk{Design: 47})
var say []string
for _, m := range read.Milestones {
say = append(say, fmt.Sprintf("%s:%s:%v", m.ID, m.Status, m.Issues))
}
if want := fmt.Sprintf("1:open:[%d] 3-fix:open:[%d] R4d:open:[] 6:closed:[%d]", a, c, d); strings.Join(say, " ") != want {
t.Fatalf("got %s, want %s", strings.Join(say, " "), want)
}
if read.Milestones[3].Imported == nil || read.Milestones[3].Imported.Path != rec.path {
t.Fatalf("%+v", read.Milestones[3])
}
again, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 47}, rec)
if err != nil || again.Made+again.Linked+again.Closed != 0 || !strings.HasPrefix(again.Said, "Changed nothing") {
t.Fatalf("again %+v %v", again, err)
}
if _, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 48}, rec); err == nil {
t.Fatal("seeded a design the record does not hold")
}
}
// Issue 1781 S1: design 47's table, as hq's branch writes it, names every issue in one row and nothing in words the
// seed would guess.
func TestDesign47sTableNamesEachIssueOnce(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "milestones", "47-delivery-from-commit-to-delivered.md"))
if err != nil {
t.Fatal(err)
}
rows, warn := parseTable(string(raw))
if len(rows) != 33 || len(warn) != 0 {
t.Fatalf("%d rows, warnings %v", len(rows), warn)
}
seen := map[uint64]string{}
for _, r := range rows {
if _, err := canonicalID(r.ID); err != nil {
t.Error(err)
}
for _, n := range r.Issues {
if other, ok := seen[n]; ok {
t.Errorf("issue %d is in rows %s and %s", n, other, r.ID)
}
seen[n] = r.ID
}
}
}