Let issue-tracker.list take numbers and worker (issue 1070)
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// list numbers answers only those issues, rows in the order given, naming a number that does not exist in
|
||||
// missing (issue 1070).
|
||||
func TestListNumbersAnswersInTheOrderGivenNamingMissing(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
a := h.open(t, "issue a", "hq")
|
||||
b := h.open(t, "issue b", "hq")
|
||||
missing := b + 1000
|
||||
|
||||
got, err := h.call(t, "list", map[string]any{"numbers": []any{float64(b), float64(a), float64(missing)}})
|
||||
if err != nil {
|
||||
t.Fatalf("list numbers: %v", err)
|
||||
}
|
||||
l, ok := got.(*Listed)
|
||||
if !ok {
|
||||
t.Fatalf("want *Listed, got %T", got)
|
||||
}
|
||||
if len(l.Rows) != 2 || l.Rows[0].Number != b || l.Rows[1].Number != a {
|
||||
t.Fatalf("want rows b then a, got %+v", l.Rows)
|
||||
}
|
||||
if len(l.Missing) != 1 || l.Missing[0] != missing {
|
||||
t.Fatalf("want missing [%d], got %v", missing, l.Missing)
|
||||
}
|
||||
}
|
||||
|
||||
// list worker answers only the issues that worker holds, in diagnosing or fixing (issue 1070).
|
||||
func TestListWorkerAnswersOnlyThatWorkersHeldIssues(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
held := h.open(t, "held by x", "hq")
|
||||
other := h.open(t, "held by someone else", "hq")
|
||||
if _, err := h.Move(MoveAsk{Number: held, Status: StatusFixing, By: "x@n"}); err != nil {
|
||||
t.Fatalf("move held: %v", err)
|
||||
}
|
||||
if _, err := h.Move(MoveAsk{Number: other, Status: StatusFixing, By: "y@n"}); err != nil {
|
||||
t.Fatalf("move other: %v", err)
|
||||
}
|
||||
|
||||
got, err := h.call(t, "list", map[string]any{"worker": "x@n"})
|
||||
if err != nil {
|
||||
t.Fatalf("list worker: %v", err)
|
||||
}
|
||||
l, ok := got.(*Listed)
|
||||
if !ok {
|
||||
t.Fatalf("want *Listed, got %T", got)
|
||||
}
|
||||
if len(l.Rows) != 1 || l.Rows[0].Number != held {
|
||||
t.Fatalf("want only x@n's held issue, got %+v", l.Rows)
|
||||
}
|
||||
}
|
||||
@@ -282,6 +282,30 @@ func (a args) list(k string) ([]string, error) {
|
||||
return nil, fmt.Errorf("%s is a list of text", k)
|
||||
}
|
||||
|
||||
// numbers is a list of whole numbers from 1, at most max entries (issue 1070's `numbers`).
|
||||
func (a args) numbers(k string, max int) ([]uint64, error) {
|
||||
v, ok := a.m[k]
|
||||
if !ok || v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
t, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s is a list of whole numbers from 1, such as [312, 313]", k)
|
||||
}
|
||||
if len(t) > max {
|
||||
return nil, fmt.Errorf("%s names %d entries; at most %d", k, len(t), max)
|
||||
}
|
||||
out := make([]uint64, 0, len(t))
|
||||
for _, e := range t {
|
||||
f, ok := e.(float64)
|
||||
if !ok || f < 1 || f > 1e9 || f != math.Trunc(f) {
|
||||
return nil, fmt.Errorf("every entry of %s is a whole number from 1, such as 312", k)
|
||||
}
|
||||
out = append(out, uint64(f))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a args) number(k string, required bool) (uint64, error) {
|
||||
v, ok := a.m[k]
|
||||
if !ok || v == nil {
|
||||
@@ -428,7 +452,9 @@ func Tools(t *Tracker) []stdio.Tool {
|
||||
"pull requests linked) is delivered; flag abandoned the issues whose worker did nothing on them for a " +
|
||||
"day with no pull request open, which a move takes over without a reason. Each row says both. Answers " +
|
||||
"what is open in one call. search ranks the issues whose title, report or notes hold its words, best first; " +
|
||||
"before pages the newest-first list: give the answer's next to read on below it.",
|
||||
"before pages the newest-first list: give the answer's next to read on below it. numbers answers only " +
|
||||
"those issues, rows in the order given, naming any that is not an issue in missing; worker (who@node) " +
|
||||
"keeps only the issues in diagnosing or fixing whose worker is exactly that.",
|
||||
Input: schema(nil, map[string]any{
|
||||
"status": map[string]any{"type": "string", "enum": Statuses, "description": "only issues at this status"},
|
||||
"owner": text("only issues located in this owner, such as mesh-controller"),
|
||||
@@ -440,9 +466,12 @@ func Tools(t *Tracker) []stdio.Tool {
|
||||
"flag": map[string]any{"type": "string", "enum": Flags, "description": "only issues delivered and not resolved, or the abandoned ones"},
|
||||
"search": text("rank the issues whose title, report or notes hold these words, best first"),
|
||||
"before": number("only issues numbered below this: the next of the page before, to read on"),
|
||||
"numbers": map[string]any{"type": "array", "items": map[string]any{"type": "integer", "minimum": 1},
|
||||
"maxItems": MostNumbers, "description": "only these issues, rows in the order given; one not an issue is named in missing"},
|
||||
"worker": text("only issues in diagnosing or fixing whose worker is exactly this, who@node"),
|
||||
}),
|
||||
Run: func(m map[string]any) (any, error) {
|
||||
a, err := readArgs("list", m, "status", "owner", "phrase", "limit", "severity", "needs", "ready", "flag", "search", "before")
|
||||
a, err := readArgs("list", m, "status", "owner", "phrase", "limit", "severity", "needs", "ready", "flag", "search", "before", "numbers", "worker")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -468,9 +497,15 @@ func Tools(t *Tracker) []stdio.Tool {
|
||||
if ask.Search, err = a.str("search"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ask.Worker, err = a.str("worker"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ask.Before, err = a.number("before", false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ask.Numbers, err = a.numbers("numbers", MostNumbers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v, given := m["ready"]; given && v != nil {
|
||||
r, err := a.flag("ready")
|
||||
if err != nil {
|
||||
|
||||
@@ -699,8 +699,17 @@ type ListAsk struct {
|
||||
Search string
|
||||
// Before keeps only the issues numbered below it: the cursor that pages the newest-first list.
|
||||
Before uint64
|
||||
// Numbers keeps only these issues, rows in the order given, combined with the other filters as AND; a
|
||||
// number that is not an issue is named in the answer's Missing (novox/hq issue 1070).
|
||||
Numbers []uint64
|
||||
// Worker keeps only the issues in diagnosing or fixing whose worker is exactly this, who@node (novox/hq
|
||||
// issue 1070).
|
||||
Worker string
|
||||
}
|
||||
|
||||
// MostNumbers is the most `numbers` may name in one list.
|
||||
const MostNumbers = 200
|
||||
|
||||
// Row is one issue in a list: short enough to read in a chat.
|
||||
type Row struct {
|
||||
Number uint64 `json:"number"`
|
||||
@@ -731,7 +740,10 @@ type Listed struct {
|
||||
Matched int `json:"matched"`
|
||||
// Next is the before that continues the list where this page ends; 0 when nothing more matches.
|
||||
Next uint64 `json:"next,omitempty"`
|
||||
Said string `json:"said"`
|
||||
// Missing is each number asked for (in Numbers) that is not an issue, in the order given (novox/hq issue
|
||||
// 1070).
|
||||
Missing []uint64 `json:"missing,omitempty"`
|
||||
Said string `json:"said"`
|
||||
}
|
||||
|
||||
// List is the issues that match, newest first, or ranked by a search: from memory once it is loaded on start
|
||||
@@ -783,6 +795,15 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
"or more words, instead")
|
||||
}
|
||||
}
|
||||
if len(a.Numbers) > MostNumbers {
|
||||
return nil, fmt.Errorf("numbers names %d entries; at most %d", len(a.Numbers), MostNumbers)
|
||||
}
|
||||
worker := ""
|
||||
if strings.TrimSpace(a.Worker) != "" {
|
||||
if worker, err = checkWorkerField("worker", a.Worker); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
limit := a.Limit
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
@@ -793,8 +814,11 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
return nil, err
|
||||
}
|
||||
now, staleAfter := t.now(), t.workerStaleAfter()
|
||||
var numbers []uint64
|
||||
if search != "" {
|
||||
usingNumbers := len(a.Numbers) > 0
|
||||
var numbers, missing []uint64
|
||||
if usingNumbers {
|
||||
numbers = a.Numbers
|
||||
} else if search != "" {
|
||||
var hits []hit
|
||||
if v := t.view(); v.ready() {
|
||||
hits = v.search(search)
|
||||
@@ -822,6 +846,12 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
lower := strings.ToLower(phrase)
|
||||
for _, n := range numbers {
|
||||
is := all[n]
|
||||
if is == nil {
|
||||
if usingNumbers {
|
||||
missing = append(missing, n)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if status != "" && is.Status != status {
|
||||
continue
|
||||
}
|
||||
@@ -841,6 +871,9 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
if owner != "" && !locatedUnder(is.LocatedIn, owner) {
|
||||
continue
|
||||
}
|
||||
if worker != "" && (is.Worker == nil || is.Worker.By != worker) {
|
||||
continue
|
||||
}
|
||||
if lower != "" && !strings.Contains(strings.ToLower(is.Title), lower) && !strings.Contains(strings.ToLower(is.Report), lower) {
|
||||
continue
|
||||
}
|
||||
@@ -857,11 +890,19 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
out.Rows = append(out.Rows, row)
|
||||
}
|
||||
}
|
||||
out.Missing = missing
|
||||
below := ""
|
||||
if a.Before != 0 {
|
||||
below = fmt.Sprintf(" below %d", a.Before)
|
||||
}
|
||||
switch {
|
||||
case usingNumbers && out.Matched == 0 && len(missing) == 0:
|
||||
out.Said = "No issue asked matches."
|
||||
case usingNumbers:
|
||||
out.Said = fmt.Sprintf("%d of %d asked match, in the order given.", out.Matched, len(a.Numbers))
|
||||
if len(missing) > 0 {
|
||||
out.Said += fmt.Sprintf(" %d of the numbers asked are not an issue.", len(missing))
|
||||
}
|
||||
case out.Matched == 0:
|
||||
out.Said = "No issue matches" + below + "."
|
||||
case search != "" && out.Matched > len(out.Rows):
|
||||
|
||||
@@ -42,15 +42,18 @@ var workerName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]*@[a-z0-9][a-z0-
|
||||
func worked(status string) bool { return status == StatusDiagnosing || status == StatusFixing }
|
||||
|
||||
// checkWorker is `by` as kept: who@node, in lower case.
|
||||
func checkWorker(s string) (string, error) {
|
||||
s, err := singleLine("by", s, MostWorker)
|
||||
func checkWorker(s string) (string, error) { return checkWorkerField("by", s) }
|
||||
|
||||
// checkWorkerField is a field holding who@node (`by`, or `worker` as list filters), as kept: in lower case.
|
||||
func checkWorkerField(field, s string) (string, error) {
|
||||
s, err := singleLine(field, s, MostWorker)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s = strings.ToLower(s)
|
||||
if !workerName.MatchString(s) {
|
||||
return "", fmt.Errorf("by %q is not who works on it and its node: give who@node, the agent or person and the "+
|
||||
"machine it works on, such as fix-399@g14 or jochen@shanks", s)
|
||||
return "", fmt.Errorf("%s %q is not who works on it and its node: give who@node, the agent or person and the "+
|
||||
"machine it works on, such as fix-399@g14 or jochen@shanks", field, s)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user