mesh/merge-gate fail: builds mesh-issues → novox; no bus step; a manifest the change touches fails the module check: modules/mesh-issues/module.json: this …
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 25
mesh/delivery superseded: a newer head of the same pull request
Every write appends one operation at the issue's last sequence before the bucket is written; a refused append writes nothing. The start carries what the bucket holds, refolds what is behind and indexes every issue in memory, so list answers from memory with a ranked search and a before cursor.
827 lines
32 KiB
Go
827 lines
32 KiB
Go
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)
|
|
}
|
|
}
|