diff --git a/modules/mesh-issues/cmd/mesh-issues/brief_test.go b/modules/mesh-issues/cmd/mesh-issues/brief_test.go new file mode 100644 index 00000000..10ebcc15 --- /dev/null +++ b/modules/mesh-issues/cmd/mesh-issues/brief_test.go @@ -0,0 +1,50 @@ +package main + +import "testing" + +// A get with brief: true answers only its last note, saying how many were left out (issue 1071). +func TestGetBriefAnswersOnlyTheLastNote(t *testing.T) { + h := newHarness(t) + n := h.open(t, "the counter skips a number", "hq") + for _, text := range []string{"first", "second", "third"} { + if _, err := h.Note(n, text); err != nil { + t.Fatalf("note: %v", err) + } + } + + got, err := h.call(t, "get", map[string]any{"number": float64(n), "brief": true}) + if err != nil { + t.Fatalf("get brief: %v", err) + } + b, ok := got.(*Brief) + if !ok { + t.Fatalf("want *Brief, got %T", got) + } + if len(b.Notes) != 1 || b.Notes[0].Text != "third" { + t.Fatalf("want only the last note (third), got %+v", b.Notes) + } + if b.NotesLeftOut != 2 { + t.Fatalf("want notes-left-out 2, got %d", b.NotesLeftOut) + } +} + +// A get without brief is unchanged (issue 1071). +func TestGetWithoutBriefIsUnchanged(t *testing.T) { + h := newHarness(t) + n := h.open(t, "the counter skips a number", "hq") + if _, err := h.Note(n, "first"); err != nil { + t.Fatalf("note: %v", err) + } + + got, err := h.call(t, "get", map[string]any{"number": float64(n)}) + if err != nil { + t.Fatalf("get: %v", err) + } + v, ok := got.(*View) + if !ok { + t.Fatalf("want *View, got %T", got) + } + if len(v.Notes) != 1 || v.Notes[0].Text != "first" { + t.Fatalf("want every note, got %+v", v.Notes) + } +} diff --git a/modules/mesh-issues/cmd/mesh-issues/main.go b/modules/mesh-issues/cmd/mesh-issues/main.go index b8d5670a..22496a1c 100644 --- a/modules/mesh-issues/cmd/mesh-issues/main.go +++ b/modules/mesh-issues/cmd/mesh-issues/main.go @@ -387,10 +387,16 @@ func Tools(t *Tracker) []stdio.Tool { "links; the issues it waits for, those that depend on it (needed-by), and whether it is ready; " + "whether its fix is delivered, as the delivery last said it (merged, held and why, delivered); and, once " + "the last of its fixes is delivered, its fix time (fix-time) from its first move to fixing, its class and " + - "fix budget, and its phases: implementing, review, checks, merge to running.", - Input: schema([]string{"number"}, map[string]any{"number": number("the issue's number")}), + "fix budget, and its phases: implementing, review, checks, merge to running. With brief: true, a short " + + "answer instead: number, title, status, severity, needs, ready, worker and since (when diagnosing or " + + "fixing), located-in, fixed-by, the delivery line, its links without who made them or when, opened, and " + + "only its last note, with notes-left-out saying how many were left out.", + Input: schema([]string{"number"}, map[string]any{ + "number": number("the issue's number"), + "brief": map[string]any{"type": "boolean", "description": "a short answer with only its last note, instead of every note and the whole triage"}, + }), Run: func(m map[string]any) (any, error) { - a, err := readArgs("get", m, "number") + a, err := readArgs("get", m, "number", "brief") if err != nil { return nil, err } @@ -398,7 +404,18 @@ func Tools(t *Tracker) []stdio.Tool { if err != nil { return nil, err } - return t.View(n) + brief, err := a.flag("brief") + if err != nil { + return nil, err + } + v, err := t.View(n) + if err != nil { + return nil, err + } + if brief { + return briefOf(v), nil + } + return v, nil }}, {Name: Seat + ".list", Description: "The issues, newest first, one short row each — number, status, owner, title — optionally only " + diff --git a/modules/mesh-issues/cmd/mesh-issues/ready.go b/modules/mesh-issues/cmd/mesh-issues/ready.go index 07e66006..71737739 100644 --- a/modules/mesh-issues/cmd/mesh-issues/ready.go +++ b/modules/mesh-issues/cmd/mesh-issues/ready.go @@ -12,6 +12,7 @@ import ( "regexp" "sort" "strings" + "time" ) // The events of ADR 0279. @@ -320,6 +321,53 @@ func (t *Tracker) View(n uint64) (*View, error) { DeliveredNotResolved: deliveredNotResolved(is), Abandoned: abandoned(is, now, staleAfter)}, nil } +// BriefLink is a link as brief answers it: its kind and target, without who made it or when (issue 1071). +type BriefLink struct { + Kind string `json:"kind"` + Target string `json:"target"` +} + +// Brief is `get` with brief: true — a short answer naming only what deciding what to do with an issue needs, +// with its last note alone and how many were left out (novox/hq issue 1071). +type Brief struct { + Number uint64 `json:"number"` + Title string `json:"title"` + Status string `json:"status,omitempty"` + Severity string `json:"severity,omitempty"` + Needs string `json:"needs,omitempty"` + Ready bool `json:"ready"` + Worker string `json:"worker,omitempty"` + Since *time.Time `json:"since,omitempty"` + LocatedIn []string `json:"located-in,omitempty"` + FixedBy []string `json:"fixed-by,omitempty"` + Delivery string `json:"delivery,omitempty"` + Links []BriefLink `json:"links,omitempty"` + Opened time.Time `json:"opened"` + Notes []Note `json:"notes,omitempty"` + NotesLeftOut int `json:"notes-left-out"` +} + +// briefOf is a View shaped for brief: true: only its last note, with how many were left out. +func briefOf(v *View) *Brief { + b := &Brief{ + Number: v.Number, Title: v.Title, Status: v.Status, Severity: v.Severity, Needs: v.Needs, + Ready: v.Ready, LocatedIn: v.LocatedIn, FixedBy: v.FixedBy, Delivery: v.Delivery, Opened: v.Opened, + } + if v.Worker != nil { + b.Worker = v.Worker.By + since := v.Worker.Since + b.Since = &since + } + for _, l := range v.Links { + b.Links = append(b.Links, BriefLink{Kind: l.Kind, Target: l.Target}) + } + if n := len(v.Notes); n > 0 { + b.Notes = []Note{v.Notes[n-1]} + b.NotesLeftOut = n - 1 + } + return b +} + // letGo says, for an issue that may have just let others go (its fix delivered, or wontfix), every issue it // made ready: a note on each, once, and the event `ready`. func (t *Tracker) letGo(dep uint64) error {