mesh/delivery superseded: a newer head of the same pull request
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 says stopped for a pull request closed unmerged too; read as merged, it was said stuck to the operator for good. Cite ADR 0284 for the worker rules, which the comments gave as 0283.
300 lines
11 KiB
Go
300 lines
11 KiB
Go
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 0284 §3).
|
|
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 0284 §3, §6).
|
|
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 != Unproven {
|
|
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 0284 §4). 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 0284 §7).
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A resolved issue may still have what it holds corrected: resolved to resolved is not refused as "not from fixing".
|
|
func TestAResolvedIssueMayBeCorrected(t *testing.T) {
|
|
h := newHarness(t)
|
|
n := h.open(t, "x", "hq")
|
|
h.fixing(t, n)
|
|
if _, err := h.Move(MoveAsk{Number: n, Status: StatusResolved, FixedBy: []string{"hq#1"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := h.Move(MoveAsk{Number: n, Status: StatusResolved, FixedBy: []string{"hq#1", "hq#2"}}); err != nil {
|
|
t.Fatalf("a correction of a resolved issue: %v", err)
|
|
}
|
|
if is, _ := h.Get(n); len(is.FixedBy) != 2 {
|
|
t.Fatalf("%+v", is.FixedBy)
|
|
}
|
|
}
|
|
|
|
// A caller's link and set keep the worker current, as a note does (ADR 0284 §4).
|
|
func TestALinkAndASetKeepTheWorkerCurrent(t *testing.T) {
|
|
for _, act := range []string{"link", "unlink", "set"} {
|
|
t.Run(act, func(t *testing.T) {
|
|
h := newHarness(t)
|
|
now := clocked(h)
|
|
n := h.open(t, "x", "hq")
|
|
other := h.open(t, "y")
|
|
if act == "unlink" {
|
|
if _, err := h.Link(LinkAsk{Number: n, Kind: LinkRelatesTo, Target: keyOf(other)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-a@g14"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
*now = now.Add(20 * time.Hour)
|
|
var err error
|
|
switch act {
|
|
case "link":
|
|
_, err = h.Link(LinkAsk{Number: n, Kind: LinkRelatesTo, Target: keyOf(other)})
|
|
case "unlink":
|
|
_, err = h.Link(LinkAsk{Number: n, Kind: LinkRelatesTo, Target: keyOf(other), Remove: true})
|
|
case "set":
|
|
_, err = h.Set(SetAsk{Number: n, Severity: "minor"})
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
*now = now.Add(10 * time.Hour) // 30 h after the start, 10 h after the act
|
|
if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-b@ace"}); err == nil {
|
|
t.Fatalf("the %s did not keep the worker current", act)
|
|
}
|
|
})
|
|
}
|
|
}
|