Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/worker_test.go
T
jschoubben 3aaccba4e2
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-issues: read merged from the pull request, not the state, so a closed one is withdrawn (hq issue 483)
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.
2026-10-11 12:24:45 +02:00

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)
}
})
}
}