mesh/delivery merged: source.main moved to novox/mesh-catalog@9c42423d5821
mesh/check checking c12f1a16db83 on 9c42423d5821
Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
192 lines
7.8 KiB
Go
192 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
|
)
|
|
|
|
// ADR 0665: the review knows its round; from round 3 only a record that is wrong asks changes; at round 5 changes
|
|
// become an ask of the operator, no review runs before the answer, and a Decline restarts the count.
|
|
|
|
func changes(i int) string {
|
|
return fmt.Sprintf(`{"verdict":"changes","findings":["finding of review %d"]}`, i)
|
|
}
|
|
|
|
// review is the i'th review of record n: its author revises it first (but for the first), then every part is
|
|
// judged with answer. It answers the questions the judge was asked.
|
|
func (w *world) review(t *testing.T, n, i int, answer string) string {
|
|
t.Helper()
|
|
p := valid(n)
|
|
p.Sections[2].Text = fmt.Sprintf("Merges land sooner; revision %d.", i)
|
|
if _, err := w.k.Propose("author@g14", p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.judge.review, w.judge.reviews = answer, nil
|
|
w.reviewAll(t)
|
|
return strings.Join(w.judge.reviews, "\n")
|
|
}
|
|
|
|
func TestTheReviewIsToldItsRoundAndRoundFiveIsTheOperators(t *testing.T) {
|
|
w := imported(t, newMemStore())
|
|
n := w.reserved(t, "author@g14")
|
|
key := Key{KindDecision, n}
|
|
for i := 1; i <= 4; i++ {
|
|
q := w.review(t, n, i, changes(i))
|
|
if !strings.Contains(q, fmt.Sprintf("This is round %d of its review", i)) || !strings.Contains(q, "At round 5 the operator decides") ||
|
|
strings.Contains(q, "From round 3 only") != (i >= 3) {
|
|
t.Fatalf("round %d's question: %.600s", i, q)
|
|
}
|
|
for j := 1; j < i; j++ { // every earlier round's findings, none of this round's
|
|
if !strings.Contains(q, fmt.Sprintf("Round %d (version", j)) || !strings.Contains(q, fmt.Sprintf("finding of review %d", j)) {
|
|
t.Errorf("round %d's question lacks the findings of round %d", i, j)
|
|
}
|
|
}
|
|
if strings.Contains(q, fmt.Sprintf("finding of review %d", i)) {
|
|
t.Errorf("round %d's question holds its own finding", i)
|
|
}
|
|
}
|
|
if e := mustGet(t, w.k.Store, key); e.Current.Status != StatusProposed || len(e.Current.Reviews) != 4 || e.Current.Round() != 5 || len(w.router.asked) != 0 {
|
|
t.Fatalf("after four rounds: %s, %d reviews, %d asks", e.Current.Status, len(e.Current.Reviews), len(w.router.asked))
|
|
}
|
|
|
|
// Round 5 asks changes: the operator is asked, showing the findings; no review runs, and no revision is taken, before the answer.
|
|
if q := w.review(t, n, 5, changes(5)); !strings.Contains(q, "This is round 5 of its review") {
|
|
t.Fatalf("round 5's question: %.300s", q)
|
|
}
|
|
if len(w.router.asked) != 1 {
|
|
t.Fatalf("asks after round 5's changes: %d", len(w.router.asked))
|
|
}
|
|
ask := w.router.asked[0]
|
|
if err := occ.Check(ask, w.clock); err != nil || !strings.Contains(ask.Explanation, "finding of review 5") || !strings.Contains(ask.Explanation, "round 5") {
|
|
t.Fatalf("the ask: %v %+v", err, ask)
|
|
}
|
|
if e := mustGet(t, w.k.Store, key); e.Current.Ask == nil || e.Current.Ask.Round != 5 || e.Current.Status != StatusProposed {
|
|
t.Fatalf("after round 5: %+v", e.Current.Ask)
|
|
}
|
|
w.judge.reviews = nil
|
|
if said := w.reviewAll(t); len(said) != 0 || len(w.judge.reviews) != 0 {
|
|
t.Fatalf("a review ran before the operator answered: %v", said)
|
|
}
|
|
p := valid(n)
|
|
p.Sections[2].Text = "Merges land sooner; revision 6."
|
|
if _, err := w.k.Propose("author@g14", p); err == nil || !strings.Contains(err.Error(), "the operator decides") {
|
|
t.Fatalf("a revision before the answer: %v", err)
|
|
}
|
|
|
|
// Decline: it stays proposed, and the revision's review is round 1.
|
|
w.router.answer(ask.ID, OptionDecline, w.clock.Add(time.Minute))
|
|
if _, err := w.k.Reconcile(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if q := w.review(t, n, 6, changes(6)); !strings.Contains(q, "This is round 1 of its review") || strings.Contains(q, "finding of review") {
|
|
t.Fatalf("after the Decline: %.400s", q)
|
|
}
|
|
if len(w.router.asked) != 1 {
|
|
t.Fatalf("round 1 asked the operator")
|
|
}
|
|
}
|
|
|
|
func TestRoundFiveApprovedByTheOperatorAcceptsTheVersionAsWritten(t *testing.T) {
|
|
w := imported(t, newMemStore())
|
|
n := w.reserved(t, "author@g14")
|
|
for i := 1; i <= 5; i++ {
|
|
w.review(t, n, i, changes(i))
|
|
}
|
|
w.router.answer(w.router.asked[0].ID, OptionApprove, w.clock.Add(time.Minute))
|
|
if _, err := w.k.Reconcile(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if e := mustGet(t, w.k.Store, Key{KindDecision, n}); e.Current.Status != StatusAccepted || e.Current.Ask.Said != AskApproved {
|
|
t.Fatalf("%s %+v", e.Current.Status, e.Current.Ask)
|
|
}
|
|
}
|
|
|
|
// From round 3 a finding that is only a remark sits under approve, and the record is accepted.
|
|
func TestARemarkUnderApproveAtRoundFourAcceptsTheRecord(t *testing.T) {
|
|
w := imported(t, newMemStore())
|
|
n := w.reserved(t, "author@g14")
|
|
for i := 1; i <= 3; i++ {
|
|
w.review(t, n, i, changes(i))
|
|
}
|
|
q := w.review(t, n, 4, `{"verdict":"approve","findings":["wording: the second section could say it shorter"]}`)
|
|
if !strings.Contains(q, "answer approve and give it as a finding under approve") {
|
|
t.Fatalf("the question: %.400s", q)
|
|
}
|
|
if e := mustGet(t, w.k.Store, Key{KindDecision, n}); e.Current.Status != StatusAccepted || len(e.Current.Reviews) != 4 {
|
|
t.Fatalf("%s, %d reviews", e.Current.Status, len(e.Current.Reviews))
|
|
}
|
|
}
|
|
|
|
// The rounds of a pending version (an insight to an accepted record) go on across its revisions.
|
|
func TestTheRoundCountGoesOnAcrossRevisionsOfAPendingInsight(t *testing.T) {
|
|
w := imported(t, newMemStore())
|
|
n := w.reserved(t, "author@g14")
|
|
w.accepted(t, "author@g14", valid(n))
|
|
var q string
|
|
for i := 1; i <= 3; i++ {
|
|
p := valid(n)
|
|
p.Insight = "it said only that merges land sooner"
|
|
p.Sections[2].Text = fmt.Sprintf("Merges land sooner, revision %d.", i)
|
|
if _, err := w.k.Propose("author@g14", p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.judge.review, w.judge.reviews = changes(i), nil
|
|
w.reviewAll(t)
|
|
q = strings.Join(w.judge.reviews, "\n")
|
|
}
|
|
if !strings.Contains(q, "This is round 3 of its review") {
|
|
t.Fatalf("the third revision of the insight: %.300s", q)
|
|
}
|
|
}
|
|
|
|
// A pending version of an active effort: revisions and a closing keep the count (ADR 0665 §1), `get` says the round and
|
|
// the ask after round 5, and a close is refused while that ask waits (§3).
|
|
func TestARoundCountSurvivesAClosingAndAClosingWaitsForTheOperatorsAnswer(t *testing.T) {
|
|
w := imported(t, newMemStore())
|
|
n := w.effort(t, StatusActive)
|
|
revise := func(i int, answer string) string {
|
|
next := effortAsk(n, StatusActive, Section{Name: "00-overview.md", Text: fmt.Sprintf("What a roadmap entry would be, revision %d.", i)})
|
|
if _, err := w.k.Propose("author@g14", next); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.judge.review, w.judge.reviews = answer, nil
|
|
w.reviewAll(t)
|
|
return strings.Join(w.judge.reviews, "\n")
|
|
}
|
|
for i := 1; i <= 2; i++ {
|
|
revise(i, changes(i))
|
|
}
|
|
if g, err := w.k.Get(GetAsk{Kind: KindResearch, Number: n}); err != nil || g.Round != 3 {
|
|
t.Fatalf("get after two rounds: %+v, %v", g, err)
|
|
}
|
|
if g, err := w.k.Get(GetAsk{Kind: KindDecision, Number: 276}); err != nil || g.Round != 0 {
|
|
t.Fatalf("get of an accepted record shows round %d, %v", g.Round, err)
|
|
}
|
|
if _, err := w.k.Close("lead@shanks", CloseAsk{Number: n, To: StatusIdea, Note: "Parked: nothing carries it this month."}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.judge.review, w.judge.reviews = "", nil
|
|
w.reviewAll(t)
|
|
if q := strings.Join(w.judge.reviews, "\n"); !strings.Contains(q, "This is round 3 of its review") || !strings.Contains(q, "finding of review 2") {
|
|
t.Fatalf("the closing's question: %.500s", q)
|
|
}
|
|
|
|
m := w.effort(t, StatusActive)
|
|
n = m
|
|
for i := 1; i <= 5; i++ {
|
|
revise(i, changes(i))
|
|
}
|
|
g, err := w.k.Get(GetAsk{Kind: KindResearch, Number: n})
|
|
if err != nil || !strings.Contains(g.Waiting, "after round 5 asked changes") {
|
|
t.Fatalf("get after round 5: %+v, %v", g.Waiting, err)
|
|
}
|
|
if _, err := w.k.Close("lead@shanks", CloseAsk{Number: n, To: StatusIdea, Note: "Parked: nothing carries it this month."}); err == nil ||
|
|
!strings.Contains(err.Error(), "the operator decides") {
|
|
t.Fatalf("a close before the answer: %v", err)
|
|
}
|
|
}
|