mesh-issues: an issue says who works on it, and resolves only from fixing (hq issue 399)
mesh/delivery-group group feat/399-fixing-status checking: 0 of 2 member(s) ready
mesh/merge-gate pass: builds mesh-issues → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
mesh/delivery-group group feat/399-fixing-status checking: 0 of 2 member(s) ready
mesh/merge-gate pass: builds mesh-issues → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
Two agents could start on one issue without seeing each other, and an issue could be resolved without anyone having said they started its fix. A move to diagnosing or fixing now names by, the worker as who@node; get and list show the worker with since and stale; another worker is refused while it is current unless take-over says why (noted); a worker untouched for worker-stale-after (24h) may be taken over from without a reason. Resolved is reached only from fixing. The moved event carries by, since and caller. Fixing counts as not closed for the triage and the duplicate check. hq ADR 0283.
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// clocked gives a harness a clock the test moves.
|
||||
func clocked(h *harness) *time.Time {
|
||||
now := time.Date(2026, 10, 10, 12, 0, 0, 0, time.UTC)
|
||||
h.Tracker.Now = func() time.Time { return now }
|
||||
return &now
|
||||
}
|
||||
|
||||
// A move to diagnosing or fixing by someone else while the worker is current is refused, naming the worker, and
|
||||
// changes nothing (ADR 0283 §1).
|
||||
func TestAnotherWorkerIsRefusedNamingTheWorker(t *testing.T) {
|
||||
for _, to := range []string{StatusDiagnosing, StatusFixing} {
|
||||
t.Run(to, func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
now := clocked(h)
|
||||
n := h.open(t, "the counter skips a number", "hq")
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusDiagnosing, By: "fix-a@g14"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
*now = now.Add(23 * time.Hour)
|
||||
before, _ := h.mem.Get(IssuesState, keyOf(n))
|
||||
events := len(h.said())
|
||||
_, err := h.Move(MoveAsk{Number: n, Status: to, By: "fix-b@ace"})
|
||||
if err == nil || !strings.Contains(err.Error(), "fix-a@g14") || !strings.Contains(err.Error(), "take-over") {
|
||||
t.Fatalf("want refused naming the worker, got %v", err)
|
||||
}
|
||||
after, _ := h.mem.Get(IssuesState, keyOf(n))
|
||||
if after.Revision != before.Revision || len(h.said()) != events {
|
||||
t.Fatal("a refused move changed the issue or said an event")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The worker moves on from diagnosing to fixing as the same worker; its since stands.
|
||||
func TestTheWorkerMovesOn(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
now := clocked(h)
|
||||
n := h.open(t, "x", "hq")
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusDiagnosing, By: "fix-a@g14"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
since := *now
|
||||
*now = now.Add(time.Hour)
|
||||
m, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "FIX-A@g14"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
is, _ := h.Get(n)
|
||||
if is.Worker.By != "fix-a@g14" || !is.Worker.Since.Equal(since) || !is.Worker.Active.Equal(*now) || m.By != "fix-a@g14" ||
|
||||
m.Since == nil || !m.Since.Equal(since) || m.TookOverFrom != "" {
|
||||
t.Fatalf("worker %+v, answer %+v", is.Worker, m)
|
||||
}
|
||||
// Naming the fix while it stays fixing is the worker's too.
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, FixedBy: []string{"hq#1"}}); err == nil ||
|
||||
!strings.Contains(err.Error(), "needs by") {
|
||||
t.Fatalf("a move to fixing without by: %v", err)
|
||||
}
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-b@ace", FixedBy: []string{"hq#1"}}); err == nil {
|
||||
t.Fatal("another named the fix while the worker was current")
|
||||
}
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-a@g14", FixedBy: []string{"hq#1"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-a@g14", TakeOver: "mine"}); err == nil ||
|
||||
!strings.Contains(err.Error(), "already") {
|
||||
t.Fatalf("a take-over of one's own work: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A take-over gives a reason, the reason is noted, and the moved event says from whom (ADR 0283 §1, §4).
|
||||
func TestATakeOverNeedsAReasonAndIsNoted(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
clocked(h)
|
||||
n := h.open(t, "x", "hq")
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-a@g14"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "jochen@shanks", TakeOver: "the operator finishes it by hand"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
is, _ := h.Get(n)
|
||||
if is.Worker.By != "jochen@shanks" || m.TookOverFrom != "fix-a@g14" || len(is.Notes) != 1 ||
|
||||
!strings.Contains(is.Notes[0].Text, "from fix-a@g14") || !strings.Contains(is.Notes[0].Text, "the operator finishes it by hand") ||
|
||||
is.Notes[0].By != "jochen@shanks" {
|
||||
t.Fatalf("worker %+v, notes %+v, answer %+v", is.Worker, is.Notes, m)
|
||||
}
|
||||
ev := h.said()
|
||||
last := ev[len(ev)-1]
|
||||
if last.Event != EventMoved || last.Body["by"] != "jochen@shanks" || last.Body["took-over-from"] != "fix-a@g14" ||
|
||||
last.Body["take-over"] != "the operator finishes it by hand" || last.Body["caller"] != Unproven {
|
||||
t.Fatalf("event %+v", last)
|
||||
}
|
||||
}
|
||||
|
||||
// A worker with nothing done on it past worker-stale-after is stale: list and get say so, and it may be taken over
|
||||
// without a reason, noted (ADR 0283 §2). A caller's note keeps it live; the tracker's own does not.
|
||||
func TestAStaleWorkerIsShownAndMayBeTakenOverWithoutAReason(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
now := clocked(h)
|
||||
n := h.open(t, "x", "hq")
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-a@g14"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
since := *now
|
||||
*now = now.Add(20 * time.Hour)
|
||||
if _, err := h.Note(n, "still on it: the counter's revision is read twice"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
*now = now.Add(10 * time.Hour) // 30 h after the start, 10 h after the worker's note
|
||||
if _, err := h.note(GroomBy, n, "Triage: a note of the tracker's own"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l, _ := h.List(ListAsk{})
|
||||
if r := l.Rows[0]; r.Worker != "fix-a@g14" || r.Since == nil || !r.Since.Equal(since) || r.Stale {
|
||||
t.Fatalf("row %+v: the worker's note keeps the worker current", r)
|
||||
}
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-b@ace"}); err == nil {
|
||||
t.Fatal("a current worker was taken over without a reason")
|
||||
}
|
||||
*now = now.Add(15 * time.Hour) // 25 h after the worker's note; the tracker's note 15 h ago does not count
|
||||
l, _ = h.List(ListAsk{})
|
||||
if r := l.Rows[0]; !r.Stale || r.Worker != "fix-a@g14" {
|
||||
t.Fatalf("row %+v: want stale", r)
|
||||
}
|
||||
v, _ := h.View(n)
|
||||
if !v.WorkerStale {
|
||||
t.Fatalf("get %+v: want worker-stale", v)
|
||||
}
|
||||
m, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-b@ace"})
|
||||
if err != nil {
|
||||
t.Fatalf("a stale worker taken over from without a reason: %v", err)
|
||||
}
|
||||
is, _ := h.Get(n)
|
||||
last := is.Notes[len(is.Notes)-1]
|
||||
if m.TookOverFrom != "fix-a@g14" || is.Worker.By != "fix-b@ace" || !strings.Contains(last.Text, "stale") {
|
||||
t.Fatalf("answer %+v, notes %+v", m, is.Notes)
|
||||
}
|
||||
l, _ = h.List(ListAsk{})
|
||||
if r := l.Rows[0]; r.Stale || r.Worker != "fix-b@ace" {
|
||||
t.Fatalf("row %+v after the take-over", r)
|
||||
}
|
||||
}
|
||||
|
||||
// The bound is the setting worker-stale-after.
|
||||
func TestTheStaleBoundIsTheSetting(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
now := clocked(h)
|
||||
h.settings.WorkerStaleAfter = "1h"
|
||||
n := h.open(t, "x", "hq")
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusDiagnosing, By: "fix-a@g14"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
*now = now.Add(59 * time.Minute)
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusDiagnosing, By: "fix-b@ace"}); err == nil {
|
||||
t.Fatal("taken under the bound")
|
||||
}
|
||||
*now = now.Add(2 * time.Minute)
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusDiagnosing, By: "fix-b@ace"}); err != nil {
|
||||
t.Fatalf("past the bound: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A move to a status that has no worker ends the work, and the event says whose ended.
|
||||
func TestAMoveOnEndsTheWork(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
clocked(h)
|
||||
n := h.open(t, "x", "hq")
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusDiagnosing, By: "fix-a@g14"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m, err := h.Move(MoveAsk{Number: n, Status: StatusLocated})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
is, _ := h.Get(n)
|
||||
ev := h.said()
|
||||
if is.Worker != nil || m.Released != "fix-a@g14" || ev[len(ev)-1].Body["released"] != "fix-a@g14" || m.By != Unproven {
|
||||
t.Fatalf("worker %+v, answer %+v, event %+v", is.Worker, m, ev[len(ev)-1])
|
||||
}
|
||||
// Anyone may start on it now, without a take-over.
|
||||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-b@ace"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// The duplicate check and the triage treat fixing as not closed, like diagnosing and located (ADR 0283 §5).
|
||||
func TestFixingIsNotClosed(t *testing.T) {
|
||||
for _, s := range Statuses {
|
||||
want := s != StatusResolved && s != StatusWontfix
|
||||
if got := unresolved(&Issue{Status: s}); got != want {
|
||||
t.Errorf("unresolved at %s: %v, want %v", s, got, want)
|
||||
}
|
||||
}
|
||||
h := newHarness(t)
|
||||
n := h.open(t, "x", "hq")
|
||||
h.fixing(t, n)
|
||||
l, _ := h.List(ListAsk{Status: StatusFixing})
|
||||
if len(l.Rows) != 1 || l.Rows[0].Number != n || l.Rows[0].Worker != "fix@g14" {
|
||||
t.Fatalf("%+v", l)
|
||||
}
|
||||
}
|
||||
|
||||
// Over the verb as the tool runner calls it: by and take-over are arguments, and fixing is a status the schema
|
||||
// offers.
|
||||
func TestTheMoveVerbTakesByAndTakeOver(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
clocked(h)
|
||||
n := h.open(t, "x", "hq")
|
||||
if _, err := h.call(t, "move", map[string]any{"number": n, "status": "fixing", "by": "fix-a@g14"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := h.call(t, "move", map[string]any{"number": n, "status": "fixing", "by": "fix-b@ace"}); err == nil {
|
||||
t.Fatal("taken without a reason over the verb")
|
||||
}
|
||||
if _, err := h.call(t, "move", map[string]any{"number": n, "status": "fixing", "by": "fix-b@ace", "take-over": "fix-a's session ended"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := h.call(t, "get", map[string]any{"number": n})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _ := json.Marshal(got)
|
||||
if !strings.Contains(string(raw), `"worker":{"by":"fix-b@ace"`) {
|
||||
t.Fatalf("get %s", raw)
|
||||
}
|
||||
for _, tool := range Tools(h.Tracker) {
|
||||
if tool.Name != Seat+".move" {
|
||||
continue
|
||||
}
|
||||
raw, _ := json.Marshal(tool.Input)
|
||||
if !strings.Contains(string(raw), `"fixing"`) || !strings.Contains(string(raw), `"take-over"`) ||
|
||||
!strings.Contains(tool.Description, "only from fixing") {
|
||||
t.Fatalf("move's schema %s", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user