package main import ( "encoding/json" "errors" "reflect" "strings" "sync" "testing" stdio "git.novox.be/novox/mesh-sdk/go" ) // stubJudge answers the triage's two questions as a test says, and keeps every question asked. The verdict's // estimates default to minor, none and no owner. type stubJudge struct { mu sync.Mutex shortlist []uint64 verdict map[string]any err error asked []string } func (s *stubJudge) judge(question string, schema map[string]any, model string) (json.RawMessage, string, error) { s.mu.Lock() defer s.mu.Unlock() s.asked = append(s.asked, question) if s.err != nil { return nil, "", s.err } if _, second := schema["properties"].(map[string]any)["verdict"]; second { v := map[string]any{"verdict": "none", "issue": 0, "certain": false, "reason": "", "severity": "minor", "severity_reason": "a contained fix", "needs": "none", "needs_reason": "an agent can fix it", "owner": "", "owner_reason": "", "depends_on": []any{}, "relates_to": []any{}} for k, x := range s.verdict { v[k] = x } raw, _ := json.Marshal(v) return raw, "the agent on control (" + model + ")", nil } raw, _ := json.Marshal(map[string]any{"candidates": s.shortlist}) return raw, "the agent on control (" + model + ")", nil } func (s *stubJudge) questions() int { s.mu.Lock() defer s.mu.Unlock() return len(s.asked) } func withJudge(h *harness, s *stubJudge) *stubJudge { h.Judge = s.judge return s } func get(t *testing.T, h *harness, n uint64) *Issue { t.Helper() is, err := h.Get(n) if err != nil { t.Fatal(err) } return is } func triageNotes(is *Issue) []string { var out []string for _, n := range is.Notes { if byTriage(n) { out = append(out, n.Text) } } return out } func resolve(t *testing.T, h *harness, n uint64, fixedBy ...string) { t.Helper() if len(fixedBy) == 0 { fixedBy = []string{"mesh-catalog#1"} } h.fixing(t, n) if _, err := h.Move(MoveAsk{Number: n, Status: StatusResolved, LocatedIn: []string{"mesh-catalog modules/records"}, FixedBy: fixedBy}); err != nil { t.Fatal(err) } } func events(h *harness, skip ...string) []string { var out []string for _, e := range h.said() { if !contains(skip, e.Event) { out = append(out, e.Event) } } return out } func ptr[T any](v T) *T { return &v } // A clear duplicate: the triage is kept first; the newer gets its values, is linked duplicates, its report kept // on the older, and moves to wontfix "duplicate of N" last; the older stays open. func TestAClearDuplicateIsClosedAndItsReportKeptOnTheOlder(t *testing.T) { h := newHarness(t) older := h.open(t, "the counter skips a number") other := h.open(t, "the desk shows nothing") newer := h.open(t, "a number is skipped by the counter") s := withJudge(h, &stubJudge{shortlist: []uint64{older}, verdict: map[string]any{ "verdict": "duplicate", "issue": older, "certain": true, "reason": "both say the counter skips a number"}}) if err := h.Check(newer); err != nil { t.Fatal(err) } n := get(t, h, newer) if n.Status != StatusWontfix || n.Reason != "duplicate of 365" || n.Triage == nil || n.Severity != "minor" || n.Needs != "none" { t.Fatalf("newer %s %q %+v %s %s", n.Status, n.Reason, n.Triage, n.Severity, n.Needs) } if len(n.Links) != 1 || n.Links[0].Kind != LinkDuplicates || n.Links[0].Target != "365" || n.Links[0].By != GroomBy { t.Fatalf("links %+v", n.Links) } notes := triageNotes(n) if len(notes) != 1 || !strings.HasPrefix(notes[0], "Triage of this issue: duplicate of 365.") || !strings.Contains(notes[0], "the agent on control (sonnet)") || !strings.Contains(notes[0], "Severity: minor — a contained fix") { t.Fatalf("newer's notes %q", notes) } o := get(t, h, older) if o.Status != StatusOpen || len(o.Links) != 0 || o.Severity != "" { t.Fatalf("the older moved, was linked or set: %s %+v", o.Status, o.Links) } notes = triageNotes(o) if len(notes) != 1 || !strings.HasPrefix(notes[0], "Triage: issue 367 was opened as a duplicate") || !strings.Contains(notes[0], "seen: a number is skipped by the counter") { t.Fatalf("older's notes %q", notes) } if len(get(t, h, other).Notes) != 0 { t.Fatal("an issue the verdict did not name was written") } if got := events(h, EventOpened); !reflect.DeepEqual(got, []string{EventTriaged, EventNoted, EventSet, EventLinked, EventNoted, EventMoved}) { t.Fatalf("order %v", got) } if s.questions() != 2 { t.Fatalf("%d questions", s.questions()) } } // Related, or a duplicate not certain: linked relates-to and noted on both; nothing moves. func TestRelatedOrNotCertainIsLinkedAndNotedAndNothingMoves(t *testing.T) { for _, v := range []map[string]any{ {"verdict": "related", "issue": 365, "certain": true, "reason": "the same counter, another symptom"}, {"verdict": "duplicate", "issue": 365, "certain": false, "reason": "it may be the same skip"}, } { h := newHarness(t) older := h.open(t, "the counter skips a number") newer := h.open(t, "the counter is slow") withJudge(h, &stubJudge{shortlist: []uint64{older}, verdict: v}) if err := h.Check(newer); err != nil { t.Fatal(err) } n, o := get(t, h, newer), get(t, h, older) if n.Status != StatusOpen || o.Status != StatusOpen { t.Fatalf("%v: a status moved: %s %s", v, n.Status, o.Status) } if len(n.Links) != 1 || n.Links[0].Kind != LinkRelatesTo || n.Links[0].Target != "365" { t.Fatalf("%v: links %+v", v, n.Links) } if nn := triageNotes(n); len(nn) != 1 || !strings.HasPrefix(nn[0], "Triage of this issue: not a duplicate; closest is 365.") || !strings.Contains(nn[0], "Relates to 365") { t.Fatalf("%v: %q", v, nn) } if on := triageNotes(o); len(on) != 1 || !strings.HasPrefix(on[0], "Triage: issue 366 relates to this one") { t.Fatalf("%v: %q", v, on) } } } // None: one note and the values; an empty shortlist still asks the second question, and the first issue of all // asks only it. func TestAnIssueThatRepeatsNothingGainsOneNoteAndItsValues(t *testing.T) { h := newHarness(t) a := h.open(t, "the counter skips a number") b := h.open(t, "the desk shows nothing") newer := h.open(t, "a backup took an hour") s := withJudge(h, &stubJudge{verdict: map[string]any{"severity": "major", "needs": "operator", "needs_reason": "only the operator knows the backup window", "owner": "mesh-catalog modules/records", "owner_reason": "it reads the record"}}) if err := h.Check(newer); err != nil { t.Fatal(err) } n := get(t, h, newer) if n.Status != StatusOpen || len(n.Links) != 0 || n.Severity != "major" || n.Needs != "operator" || n.SuggestedOwner != "mesh-catalog modules/records" { t.Fatalf("%+v", n) } if nn := triageNotes(n); len(nn) != 1 || !strings.HasPrefix(nn[0], "Triage of this issue: none of 2 older issues") || !strings.Contains(nn[0], "Needs: operator — only the operator knows") || !strings.Contains(nn[0], "Suggested owner: mesh-catalog modules/records") { t.Fatalf("%q", nn) } if len(get(t, h, a).Notes)+len(get(t, h, b).Notes) != 0 || s.questions() != 2 { t.Fatalf("older issues written, or %d questions", s.questions()) } h2 := newHarness(t) first := h2.open(t, "the first") s2 := withJudge(h2, &stubJudge{verdict: map[string]any{"owner": "not-a-registered-owner"}}) if err := h2.Check(first); err != nil { t.Fatal(err) } f := get(t, h2, first) if nn := triageNotes(f); len(nn) != 1 || s2.questions() != 1 || f.Severity != "minor" || f.SuggestedOwner != "" { t.Fatalf("%q, %d questions, %+v", nn, s2.questions(), f) } } // The triage fills only what is empty: a value the issuer gave stands, and the note says the triage disagrees. func TestAValueTheIssuerGaveStandsAndADisagreementIsNoted(t *testing.T) { h := newHarness(t) o, err := h.Open(OpenAsk{Title: "the controller loses a plan", Report: "seen: a plan vanished", Severity: "critical"}) if err != nil { t.Fatal(err) } withJudge(h, &stubJudge{verdict: map[string]any{"severity": "minor", "severity_reason": "nothing is lost", "needs": "decision"}}) if err := h.Check(o.Number); err != nil { t.Fatal(err) } is := get(t, h, o.Number) if is.Severity != "critical" || is.Needs != "decision" { t.Fatalf("%s %s", is.Severity, is.Needs) } if nn := triageNotes(is); len(nn) != 1 || !strings.Contains(nn[0], "Severity: the triage says minor — nothing is lost; the value given, critical, stands.") { t.Fatalf("%q", nn) } if _, err := h.Open(OpenAsk{Title: "x", Report: "y", Severity: "urgent"}); err == nil { t.Fatal("a severity not of its kind was taken") } } // A resolved or wontfix issue is never offered, and a verdict naming one — or any issue not offered, or a // newer one — changes nothing and fails loudly. func TestAResolvedIssueIsNeverOfferedOrTouched(t *testing.T) { h := newHarness(t) resolved := h.open(t, "the counter skips a number") resolve(t, h, resolved) closed := h.open(t, "the counter skips again") if _, err := h.Move(MoveAsk{Number: closed, Status: StatusWontfix, Reason: "not ours"}); err != nil { t.Fatal(err) } open := h.open(t, "the desk shows nothing") newer := h.open(t, "a number is skipped") later := h.open(t, "a later one") before := map[uint64]int{} for _, n := range []uint64{resolved, closed, open} { before[n] = len(get(t, h, n).Notes) } for _, named := range []uint64{resolved, closed, later, 999} { s := withJudge(h, &stubJudge{shortlist: []uint64{resolved, closed, open, later}, verdict: map[string]any{ "verdict": "duplicate", "issue": named, "certain": true, "reason": "the same", "depends_on": []any{map[string]any{"issue": resolved, "reason": "x"}}}}) err := h.Check(newer) if err == nil || !strings.Contains(err.Error(), "not offered") { t.Fatalf("a verdict naming %d: %v", named, err) } for _, q := range s.asked { if strings.Contains(q, "365: ") || strings.Contains(q, "366: ") || strings.Contains(q, "OLDER ISSUE 365") || strings.Contains(q, "OLDER ISSUE 366") || strings.Contains(q, "369") { t.Fatalf("a resolved, wontfix or newer issue was offered: %s", q) } } } for n, was := range before { is := get(t, h, n) if len(is.Notes) != was || len(is.Links) != 0 { t.Fatalf("issue %d was changed", n) } } n := get(t, h, newer) if n.Status != StatusOpen || len(n.Links) != 0 || recorded(n) != nil { t.Fatalf("the newer was acted on: %s %+v", n.Status, n.Links) } if _, err := h.apply(newer, &Verdict{Verdict: VerdictDuplicate, Issue: resolved, Certain: true, Reason: "x", By: "y"}); err == nil { t.Fatal("applied against a resolved issue") } if _, err := h.apply(open, &Verdict{Verdict: VerdictDuplicate, Issue: newer, Certain: true, Reason: "x", By: "y"}); err == nil { t.Fatal("closed the older of two") } if err := h.Check(resolved); err != nil || len(get(t, h, resolved).Notes) != before[resolved] { t.Fatalf("a resolved issue was triaged: %v", err) } } // Once per issue: the same event twice judges once; a triage kept and not applied is finished without judging; // a note in the triage's words by anybody else is not the triage. func TestTheSameEventTwiceJudgesOnceAndFinishesWhatIsMissing(t *testing.T) { h := newHarness(t) older := h.open(t, "the counter skips a number") newer := h.open(t, "a number is skipped") s := withJudge(h, &stubJudge{shortlist: []uint64{older}, verdict: map[string]any{ "verdict": "duplicate", "issue": older, "certain": true, "reason": "the same skip"}}) ev := stdio.Envelope{Key: "mesh-issues.opened", Body: json.RawMessage(`{"number":366}`)} for i := 0; i < 2; i++ { if err := h.OnOpened(ev); err != nil { t.Fatal(err) } } if s.questions() != 2 || len(triageNotes(get(t, h, newer))) != 1 || len(triageNotes(get(t, h, older))) != 1 { t.Fatalf("judged again or written twice: %d questions", s.questions()) } h2 := newHarness(t) o2 := h2.open(t, "the counter skips a number") n2 := h2.open(t, "a number is skipped") kept := &Verdict{Verdict: VerdictDuplicate, Issue: o2, Certain: true, Reason: "the same", Severity: "minor", Needs: "none", By: "the agent"} if _, err := h2.change(n2, Act{Op: EventTriaged, By: GroomBy}, func(i *Issue) error { i.Triage = kept; return nil }); err != nil { t.Fatal(err) } s2 := withJudge(h2, &stubJudge{err: errors.New("must not be asked")}) if err := h2.Check(n2); err != nil { t.Fatal(err) } if n := get(t, h2, n2); s2.questions() != 0 || n.Status != StatusWontfix || len(n.Links) != 1 || len(triageNotes(get(t, h2, o2))) != 1 { t.Fatalf("not finished, or judged again: %s %+v", n.Status, n.Links) } h3 := newHarness(t) h3.open(t, "the counter skips a number") n3 := h3.open(t, "a number is skipped") if _, err := h3.Note(n3, "Duplicate check: duplicate of 365."); err != nil { t.Fatal(err) } withJudge(h3, &stubJudge{}) if err := h3.Check(n3); err != nil { t.Fatal(err) } if n := get(t, h3, n3); n.Status != StatusOpen { t.Fatal("a caller's note closed an issue") } } // An issue the duplicate check of ADR 0278 judged is not judged again on its event; the backlog's pass adds the // rest and keeps its verdict. func TestAnIssueTheDuplicateCheckJudgedKeepsItsVerdictAndGainsTheRest(t *testing.T) { h := newHarness(t) h.open(t, "the counter skips a number") n := h.open(t, "a number is skipped") if _, err := h.note(LegacyBy, n, "Duplicate check: related to 365.\n\nWhy: the same counter."); err != nil { t.Fatal(err) } s := withJudge(h, &stubJudge{shortlist: []uint64{365}, verdict: map[string]any{"verdict": "duplicate", "issue": 365, "certain": true, "reason": "the same"}}) if err := h.Check(n); err != nil || s.questions() != 0 { t.Fatalf("judged again on its event: %v, %d", err, s.questions()) } var run func() out, err := h.Groom(GroomAsk{}, func(f func()) { run = f }) if err != nil { t.Fatal(err) } run() if _, err := h.Groom(GroomAsk{Pass: out.Pass.ID, Confirm: true}, nil); err != nil { t.Fatal(err) } is := get(t, h, n) if is.Status != StatusOpen || is.Severity != "minor" || is.Triage == nil || is.Triage.Verdict != VerdictRelated { t.Fatalf("%s %s %+v", is.Status, is.Severity, is.Triage) } } // A judgement that fails is loud: the event is not taken, and one note says why however often it comes. func TestAFailedJudgementLeavesOneNoteAndIsNotTaken(t *testing.T) { for _, s := range []*stubJudge{ {err: errors.New("the agent on control did not answer within 25s")}, {shortlist: []uint64{365}, verdict: map[string]any{"verdict": "maybe", "issue": 365, "certain": true, "reason": "?"}}, {shortlist: []uint64{365}, verdict: map[string]any{"verdict": "duplicate", "issue": 365, "certain": true, "reason": ""}}, {shortlist: []uint64{365}, verdict: map[string]any{"severity": "urgent"}}, } { h := newHarness(t) older := h.open(t, "the counter skips a number") newer := h.open(t, "a number is skipped") withJudge(h, s) ev := stdio.Envelope{Key: "mesh-issues.opened", Body: json.RawMessage(`{"number":366}`)} for i := 0; i < 5; i++ { if err := h.OnOpened(ev); err == nil { t.Fatal("a failed judgement was taken") } } notes := triageNotes(get(t, h, newer)) if len(notes) != 1 || !strings.HasPrefix(notes[0], "Triage: could not judge") || !strings.Contains(notes[0], "max-deliveries") { t.Fatalf("%q", notes) } if n := get(t, h, newer); n.Status != StatusOpen || len(n.Links) != 0 || n.Severity != "" || len(get(t, h, older).Notes) != 0 { t.Fatal("a failed judgement changed something") } } h := newHarness(t) h.open(t, "one") s := withJudge(h, &stubJudge{}) h.settings.DuplicateCheck = "off" if err := h.OnOpened(stdio.Envelope{Body: json.RawMessage(`{"number":365}`)}); err != nil || s.questions() != 0 || len(get(t, h, 365).Notes) != 0 { t.Fatalf("triaged while off: %v", err) } h.settings.DuplicateCheck = "" if err := h.OnOpened(stdio.Envelope{Body: json.RawMessage(`{}`)}); err != nil { t.Fatal(err) } } // The triage links what an issue depends on and notes the older; never a cycle, never a closed target. The verb // refuses a cycle of two and of three. func TestTheTriageLinksDependenciesButNoCycleAndNoClosedIssue(t *testing.T) { h := newHarness(t) a := h.open(t, "the bus drops a grant") b := h.open(t, "the controller cannot issue a grant") c := h.open(t, "a closed one") if _, err := h.Move(MoveAsk{Number: c, Status: StatusWontfix, Reason: "x"}); err != nil { t.Fatal(err) } if _, err := h.Link(LinkAsk{Number: a, Kind: LinkDependsOn, Target: keyOf(b)}); err != nil { t.Fatal(err) } newer := h.open(t, "a module cannot reach its grant") withJudge(h, &stubJudge{shortlist: []uint64{a, b}, verdict: map[string]any{ "depends_on": []any{map[string]any{"issue": b, "reason": "it needs the grant first"}, map[string]any{"issue": c, "reason": "closed"}}, "relates_to": []any{map[string]any{"issue": a, "reason": "the same grants"}}}}) if err := h.Check(newer); err != nil { t.Fatal(err) } n := get(t, h, newer) if !hasLink(n, LinkDependsOn, b) || !hasLink(n, LinkRelatesTo, a) || hasLink(n, LinkDependsOn, c) { t.Fatalf("%+v", n.Links) } if nn := triageNotes(get(t, h, b)); len(nn) != 1 || !strings.HasPrefix(nn[0], "Triage: issue 368 depends on this one") { t.Fatalf("%q", nn) } if _, err := h.Link(LinkAsk{Number: b, Kind: LinkDependsOn, Target: keyOf(a)}); !errors.Is(err, ErrCycle) { t.Fatalf("a cycle of two: %v", err) } if _, err := h.Link(LinkAsk{Number: b, Kind: LinkDependsOn, Target: keyOf(newer)}); !errors.Is(err, ErrCycle) { t.Fatalf("a cycle through the triage's link: %v", err) } v, err := h.View(b) if err != nil || !reflect.DeepEqual(v.NeededBy, []uint64{a, newer}) { t.Fatalf("%v %+v", err, v) } } // Ready: no dependency; one open; resolved and not delivered; resolved and delivered; wontfix. func TestAnIssueIsReadyOnlyWhenWhatItDependsOnIsDelivered(t *testing.T) { h := newHarness(t) dep := h.open(t, "the fix first") w := h.open(t, "waits for it") free := h.open(t, "depends on nothing") if _, err := h.Link(LinkAsk{Number: w, Kind: LinkDependsOn, Target: keyOf(dep)}); err != nil { t.Fatal(err) } ready := func(h *harness, n uint64) bool { v, err := h.View(n) if err != nil { t.Fatal(err) } return v.Ready } if !ready(h, free) || ready(h, w) { t.Fatal("ready wrong with an open dependency") } asked := 0 h.Deliveries = func(repo, number string) (Delivery, error) { asked++ if repo != "mesh-catalog" || number != "9" { t.Errorf("asked %s#%s", repo, number) } return Delivery{State: "ready"}, nil } resolve(t, h, dep, "novox/mesh-catalog#9", "a commit abc123") if err := h.OnMoved(stdio.Envelope{Body: json.RawMessage(`{"number":365,"to":"resolved"}`)}); err != nil { t.Fatal(err) } if asked != 1 || ready(h, w) { t.Fatalf("ready before its fix is delivered (asked %d)", asked) } tr := stdio.Envelope{Key: "mesh-delivery.transition", Body: json.RawMessage(`{"repository":"novox/mesh-catalog","number":9,"to":"delivered"}`)} for i := 0; i < 2; i++ { if err := h.OnTransition(tr); err != nil { t.Fatal(err) } } if !ready(h, w) || !reflect.DeepEqual(get(t, h, dep).Delivered, []string{"mesh-catalog#9"}) { t.Fatal("not ready once delivered") } if nn := triageNotes(get(t, h, w)); len(nn) != 1 || !strings.HasPrefix(nn[0], "Ready: issue 365") { t.Fatalf("%q", nn) } readies := 0 for _, e := range h.said() { if e.Event == EventReady { readies++ } } if readies != 1 { t.Fatalf("%d ready events", readies) } rows, err := h.List(ListAsk{Ready: ptr(true), Status: StatusOpen}) if err != nil || rows.Matched != 2 { t.Fatalf("%v %+v", err, rows) } // A fix delivered before its issue was resolved is found at the move; a wontfix dependency lets go at once. h2 := newHarness(t) d2 := h2.open(t, "fixed earlier") w2 := h2.open(t, "waits") d3 := h2.open(t, "abandoned") w3 := h2.open(t, "waits on the abandoned one") _, _ = h2.Link(LinkAsk{Number: w2, Kind: LinkDependsOn, Target: keyOf(d2)}) _, _ = h2.Link(LinkAsk{Number: w3, Kind: LinkDependsOn, Target: keyOf(d3)}) h2.Deliveries = func(string, string) (Delivery, error) { return Delivery{State: "delivered"}, nil } resolve(t, h2, d2, "mesh-controller#5") _ = h2.OnMoved(stdio.Envelope{Body: json.RawMessage(`{"number":365,"to":"resolved"}`)}) _, _ = h2.Move(MoveAsk{Number: d3, Status: StatusWontfix, Reason: "abandoned"}) _ = h2.OnMoved(stdio.Envelope{Body: json.RawMessage(`{"number":367,"to":"wontfix"}`)}) for _, n := range []uint64{w2, w3} { if !ready(h2, n) || len(triageNotes(get(t, h2, n))) != 1 { t.Fatalf("issue %d not let go", n) } } } // set changes the three values as a person decides, refuses another, and says the event; the triage never // changes what was set. func TestSetChangesWhatAPersonDecidesAndTheTriageLeavesIt(t *testing.T) { h := newHarness(t) n := h.open(t, "one") if _, err := h.call(t, "set", map[string]any{"number": n, "severity": "urgent"}); err == nil { t.Fatal("a severity not of its kind was set") } if _, err := h.call(t, "set", map[string]any{"number": n, "suggested-owner": "nowhere"}); err == nil { t.Fatal("an owner not registered was set") } if _, err := h.call(t, "set", map[string]any{"number": n}); err == nil { t.Fatal("nothing was set and not refused") } if _, err := h.call(t, "set", map[string]any{"number": n, "severity": "major", "needs": "decision"}); err != nil { t.Fatal(err) } withJudge(h, &stubJudge{verdict: map[string]any{"severity": "minor", "needs": "none"}}) if err := h.Check(n); err != nil { t.Fatal(err) } is := get(t, h, n) if is.Severity != "major" || is.Needs != "decision" { t.Fatalf("%s %s", is.Severity, is.Needs) } sets := 0 for _, e := range h.said() { if e.Event == EventSet { sets++ } } if sets != 1 { t.Fatalf("%d set events", sets) } rows, _ := h.List(ListAsk{Severity: "major", Needs: "decision"}) if rows.Matched != 1 || rows.Rows[0].Severity != "major" { t.Fatalf("%+v", rows) } } // The dry run writes no issue; confirm applies exactly its findings, each checked again. func TestTheDryRunChangesNothingAndConfirmAppliesItsFindings(t *testing.T) { h := newHarness(t) a := h.open(t, "the counter skips a number") // 365 b := h.open(t, "a number is skipped") // 366, duplicate of 365 c := h.open(t, "the counter skips numbers too") // 367, duplicate of 365 (366 is not offered: found a duplicate) d := h.open(t, "the desk is slow") // 368, related to 365, which is resolved before confirm e := h.open(t, "a backup took an hour") // 369, none verdicts := map[string]map[string]any{ "366": {"verdict": "duplicate", "issue": a, "certain": true, "reason": "same skip"}, "367": {"verdict": "duplicate", "issue": a, "certain": true, "reason": "same skip again"}, "368": {"verdict": "related", "issue": a, "certain": false, "reason": "same desk"}, } var mu sync.Mutex var offered367 string h.Judge = func(q string, schema map[string]any, model string) (json.RawMessage, string, error) { mu.Lock() defer mu.Unlock() _, second := schema["properties"].(map[string]any)["verdict"] v := map[string]any{"verdict": "none", "issue": 0, "certain": false, "reason": "", "severity": "minor", "severity_reason": "r", "needs": "none", "needs_reason": "r", "owner": "", "owner_reason": "", "depends_on": []any{}, "relates_to": []any{}} for n, x := range verdicts { if strings.Contains(q, "new issue, "+n+":") || strings.Contains(q, "NEW ISSUE, "+n+":") { if n == "367" && !second { offered367 = q } if !second { return json.RawMessage(`{"candidates":[365,366]}`), "the agent", nil } for k, y := range x { v[k] = y } } } if !second { return json.RawMessage(`{"candidates":[]}`), "the agent", nil } raw, _ := json.Marshal(v) return raw, "the agent on control (sonnet)", nil } before := h.mem.snapshot(IssuesState) var run func() out, err := h.Groom(GroomAsk{}, func(f func()) { run = f }) if err != nil { t.Fatal(err) } if _, err := h.Groom(GroomAsk{}, func(func()) {}); err == nil { t.Fatal("a second pass started while one runs") } run() if !reflect.DeepEqual(before, h.mem.snapshot(IssuesState)) { t.Fatal("the dry run changed an issue") } if offered367 == "" || strings.Contains(offered367, "366: ") { t.Fatal("an issue found a duplicate in the pass was offered again") } shown, err := h.Groom(GroomAsk{Pass: out.Pass.ID}, nil) if err != nil { t.Fatal(err) } p := shown.Pass if p.State != PassDone || p.Judged != 5 || len(p.Findings) != 5 || len(p.Failures) != 0 { t.Fatalf("%+v", p) } if p.Findings[1].Issue != b || !contains(p.Findings[1].WouldDo, "close as wontfix, duplicate of 365; keep its report on 365") || !contains(p.Findings[1].WouldDo, "severity minor") { t.Fatalf("%+v", p.Findings[1]) } resolve(t, h, a) // the older side moved on between the dry run and confirm if _, err := h.Groom(GroomAsk{Confirm: true}, nil); err == nil { t.Fatal("confirm without a pass") } applied, err := h.Groom(GroomAsk{Pass: p.ID, Confirm: true}, nil) if err != nil { t.Fatal(err) } if len(applied.Pass.Skipped) != 3 { t.Fatalf("skipped %v, applied %v", applied.Pass.Skipped, applied.Pass.Applied) } for _, n := range []uint64{b, c} { if is := get(t, h, n); is.Status != StatusOpen || len(is.Links) != 0 || is.Severity != "" { t.Fatalf("issue %d was changed against a resolved issue", n) } } if is := get(t, h, d); is.Status != StatusOpen || hasLink(is, LinkRelatesTo, a) || is.Severity != "minor" { t.Fatalf("issue %d: %+v", d, is) } if is := get(t, h, e); is.Severity != "minor" || len(triageNotes(is)) != 1 { t.Fatalf("issue %d was not triaged: %+v", e, is) } if _, err := h.Groom(GroomAsk{Pass: p.ID, Confirm: true}, nil); err == nil { t.Fatal("a pass applied twice") } } func TestConfirmClosesOnlyTheNewerAndKeepsTheOlderOpen(t *testing.T) { h := newHarness(t) a := h.open(t, "the counter skips a number") b := h.open(t, "a number is skipped") withJudge(h, &stubJudge{shortlist: []uint64{a}, verdict: map[string]any{"verdict": "duplicate", "issue": a, "certain": true, "reason": "same"}}) var run func() out, err := h.Groom(GroomAsk{}, func(f func()) { run = f }) if err != nil { t.Fatal(err) } run() if _, err := h.Groom(GroomAsk{Pass: out.Pass.ID, Confirm: true}, nil); err != nil { t.Fatal(err) } if get(t, h, a).Status != StatusOpen || get(t, h, b).Status != StatusWontfix || get(t, h, b).Reason != "duplicate of 365" { t.Fatal("not the newer alone closed") } p, _, _ := h.readPass() p.State = PassRunning _ = h.savePass(p) shown, err := h.Groom(GroomAsk{Pass: p.ID}, nil) if err != nil || shown.Pass.State != PassInterrupted { t.Fatalf("%v %+v", err, shown) } if _, err := h.Groom(GroomAsk{Pass: p.ID, Confirm: true}, nil); err == nil { t.Fatal("an interrupted pass was applied") } } // An issue the triage judged, then resolved by a person, is not touched when its event comes again. func TestATriagedIssueAPersonResolvedIsNotTouchedAgain(t *testing.T) { h := newHarness(t) older := h.open(t, "the counter skips a number") newer := h.open(t, "a number is skipped") kept := &Verdict{Verdict: VerdictDuplicate, Issue: older, Certain: true, Reason: "same"} if _, err := h.change(newer, Act{Op: EventTriaged, By: GroomBy}, func(i *Issue) error { i.Triage = kept; return nil }); err != nil { t.Fatal(err) } resolve(t, h, newer) withJudge(h, &stubJudge{err: errors.New("must not be asked")}) if err := h.Check(newer); err != nil { t.Fatal(err) } if len(get(t, h, newer).Links) != 0 || len(get(t, h, older).Notes) != 0 { t.Fatal("a resolved issue was linked, or its older noted") } } // The pass runs in the background for real, while its answer is read and a new issue is triaged; the race // detector watches. func TestThePassRunsBesideTheTriageOfNewIssues(t *testing.T) { h := newHarness(t) for i := 0; i < 6; i++ { h.open(t, "issue "+string(rune('a'+i))) } withJudge(h, &stubJudge{}) finished := make(chan struct{}) out, err := h.Groom(GroomAsk{}, func(f func()) { go func() { f(); close(finished) }() }) if err != nil { t.Fatal(err) } raw, _ := json.Marshal(out) newer := h.open(t, "a new one") if err := h.Check(newer); err != nil { t.Fatal(err) } <-finished if !strings.Contains(string(raw), out.Pass.ID) { t.Fatal(string(raw)) } p, _, _ := h.readPass() if p.State != PassDone || p.Judged != 6 { t.Fatalf("%+v", p) } if nn := triageNotes(get(t, h, newer)); len(nn) != 1 || !strings.HasPrefix(nn[0], "Triage of this issue: none of 6 older") { t.Fatalf("%q", nn) } } // After a judgement that failed, the next one still writes its verdict note (review of PR 184). func TestAFailureNoteDoesNotHideTheVerdictNote(t *testing.T) { h := newHarness(t) h.open(t, "the counter skips a number") n := h.open(t, "a number is skipped") s := withJudge(h, &stubJudge{err: errors.New("down")}) if err := h.Check(n); err == nil { t.Fatal("a failure was taken") } s.err = nil if err := h.Check(n); err != nil { t.Fatal(err) } nn := triageNotes(get(t, h, n)) if len(nn) != 2 || !strings.HasPrefix(nn[1], "Triage of this issue: none of 1") { t.Fatalf("%q", nn) } } // One delivery that lets two dependencies go says ready once; a wontfix duplicate lets go only with its original; // fixed-by is read in any case and as the forge's page; a delivery that cannot be asked is not taken. func TestReadyIsSaidOnceFollowsDuplicatesAndAskingFailsLoudly(t *testing.T) { h := newHarness(t) a := h.open(t, "a") b := h.open(t, "b") w := h.open(t, "waits on both") _, _ = h.Link(LinkAsk{Number: w, Kind: LinkDependsOn, Target: keyOf(a)}) _, _ = h.Link(LinkAsk{Number: w, Kind: LinkDependsOn, Target: keyOf(b)}) h.Deliveries = func(string, string) (Delivery, error) { return Delivery{State: "published"}, nil } resolve(t, h, a, "Mesh-Catalog#9") resolve(t, h, b, "https://forge.example/novox/mesh-catalog/pulls/9") if got := pullRequests(get(t, h, a)); !reflect.DeepEqual(got, []string{"mesh-catalog#9"}) { t.Fatalf("%v", got) } if got := pullRequests(get(t, h, b)); !reflect.DeepEqual(got, []string{"mesh-catalog#9"}) { t.Fatalf("%v", got) } tr := stdio.Envelope{Body: json.RawMessage(`{"repository":"novox/mesh-catalog","number":9,"to":"delivered"}`)} if err := h.OnTransition(tr); err != nil { t.Fatal(err) } readies := 0 for _, e := range h.said() { if e.Event == EventReady { readies++ } } if readies != 1 || len(triageNotes(get(t, h, w))) != 1 { t.Fatalf("%d ready events, notes %q", readies, triageNotes(get(t, h, w))) } h2 := newHarness(t) orig := h2.open(t, "the original") dup := h2.open(t, "its duplicate") w2 := h2.open(t, "waits on the duplicate") _, _ = h2.Link(LinkAsk{Number: w2, Kind: LinkDependsOn, Target: keyOf(dup)}) _, _ = h2.Link(LinkAsk{Number: dup, Kind: LinkDuplicates, Target: keyOf(orig)}) _, _ = h2.Move(MoveAsk{Number: dup, Status: StatusWontfix, Reason: "duplicate of 365"}) _ = h2.OnMoved(stdio.Envelope{Body: json.RawMessage(`{"number":366,"to":"wontfix"}`)}) if v, _ := h2.View(w2); v.Ready { t.Fatal("ready while the original is open") } h2.Deliveries = func(string, string) (Delivery, error) { return Delivery{}, errors.New("mesh-delivery did not answer") } resolve(t, h2, orig, "mesh-catalog#3") if err := h2.OnMoved(stdio.Envelope{Body: json.RawMessage(`{"number":365,"to":"resolved"}`)}); err == nil { t.Fatal("a delivery that could not be asked was taken") } h2.Deliveries = func(string, string) (Delivery, error) { return Delivery{State: "delivered"}, nil } if err := h2.OnMoved(stdio.Envelope{Body: json.RawMessage(`{"number":365,"to":"resolved"}`)}); err != nil { t.Fatal(err) } if v, _ := h2.View(w2); !v.Ready || len(triageNotes(get(t, h2, w2))) != 1 { t.Fatalf("not let go through the duplicate: %+v", v) } } // A verdict of related or duplicate naming issue 0 names none: read as none, not a failure. func TestAVerdictNamingIssueZeroIsNone(t *testing.T) { h := newHarness(t) h.open(t, "the counter skips a number") n := h.open(t, "the desk is slow") withJudge(h, &stubJudge{verdict: map[string]any{"verdict": "related", "issue": 0, "reason": "nothing close"}}) if err := h.Check(n); err != nil { t.Fatal(err) } is := get(t, h, n) if is.Triage == nil || is.Triage.Verdict != VerdictNone || len(is.Links) != 0 || is.Severity != "minor" { t.Fatalf("%+v", is) } }