package main import ( "context" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/asks" ) // to-be 55 §10: "A record is accepted only after the knowledge base's own judgement approves it, and, when it needs // the operator, only on the warrant — the module's tests: a proposal whose judgement answers changes, or cannot be // had, stays proposed; a needs operator record unaccepted without the warrant, accepted with it." func proposed(t *testing.T, w *world, author string, needsOperator bool) int { t.Helper() n := w.reserved(t, author) p := valid(n) p.NeedsOperator = needsOperator if needsOperator { p.Brief = &Brief{Headline: "Merges land after ninety seconds of quiet", Summary: "A merge window closes after ninety seconds without a merge, so merges land sooner."} } if _, err := w.k.Propose(author, p); err != nil { t.Fatal(err) } return n } func TestAProposalIsReviewedByTheKnowledgeBaseItselfAndAcceptedOnApprove(t *testing.T) { eachStore(t, func(t *testing.T, s Store) { w := imported(t, s) n := proposed(t, w, "author@g14", false) // Proposed, waiting for its review: nothing of it holds yet. e := mustGet(t, s, Key{KindDecision, n}) if e.Current.Status != StatusProposed || e.Current.Judging == nil || e.Current.Judging.State != JudgingDue || len(w.judge.reviews) != 0 { t.Fatalf("after propose: %s, %+v", e.Current.Status, e.Current.Judging) } said := w.reviewAll(t) if len(said) != 1 || len(w.judge.reviews) != 1 { t.Fatalf("reviews: %v, %d judgements", said, len(w.judge.reviews)) } // The judgement saw the record, the records it links and the rules — nothing else of the knowledge base. q := w.judge.reviews[0] for _, want := range []string{"The window closes after ninety seconds of quiet", "ADR 0276", "a decision has at least one clause", "Merges land sooner."} { if !strings.Contains(q, want) { t.Errorf("the review's question lacks %q", want) } } for _, never := range []string{"ADR 0131", "ADR 0011", "author@g14"} { if strings.Contains(q, never) { t.Errorf("the review's question holds %q, which the record does not link", never) } } e = mustGet(t, s, Key{KindDecision, n}) if e.Current.Status != StatusAccepted || e.Current.Clauses[0].Status != ClauseInForce || e.Current.AcceptedAt.IsZero() || e.Current.Judging != nil || len(e.Current.Reviews) != 1 || e.Current.Reviews[0].Verdict != VerdictApprove || e.Current.Reviews[0].By != "the agent on test (sonnet)" || e.Current.Reviews[0].Version != 2 { t.Fatalf("not accepted: %+v", e.Current) } // 0276 §1 is changed by the new record's clause 1, as its changes link says. c := mustGet(t, s, Key{KindDecision, 276}).Current.Clause(1) if c.Status != ClauseChanged || len(c.ChangedBy) != 1 || c.ChangedBy[0] != itoaKey(n)+" §1" { t.Fatalf("0276 §1: %+v", c) } if len(w.said.of(EventReviewed)) != 1 || len(w.said.of(EventAccepted)) != 1 || len(w.said.of(EventChanged)) != 1 { t.Fatalf("events: %v", w.said.events) } h, err := w.k.Holds("", &Key{KindDecision, 276}, 0) if err != nil { t.Fatal(err) } if !strings.Contains(h.Clauses[0].Holds, "changed by "+itoaKey(n)) { t.Fatalf("holds: %+v", h.Clauses[0]) } // Reviewed once: nothing waits any more. if said := w.reviewAll(t); len(said) != 0 || len(w.judge.reviews) != 1 { t.Fatalf("reviewed again: %v", said) } }) } func itoaKey(n int) string { return Key{KindDecision, n}.String() } // On changes the record stays proposed, the findings kept with the version they judged; only its author revises // it, and the revision is reviewed again. func TestOnChangesTheRecordStaysProposedAndItsAuthorsRevisionIsReviewedAgain(t *testing.T) { eachStore(t, func(t *testing.T, s Store) { w := imported(t, s) n := proposed(t, w, "author@g14", false) w.judge.review = `{"verdict":"changes","findings":["§2: name the bound","Consequences: say who waits"]}` w.reviewAll(t) e := mustGet(t, s, Key{KindDecision, n}) if e.Current.Status != StatusProposed || e.Current.Judging != nil || len(e.Current.Reviews) != 1 || e.Current.Reviews[0].Verdict != VerdictChanges || e.Current.Reviews[0].Findings != "§2: name the bound\nConsequences: say who waits" || e.Current.Reviews[0].Version != 2 { t.Fatalf("after changes: %+v", e.Current) } g, _ := w.k.Get(GetAsk{Number: n}) if len(g.Reviews) != 1 || g.Clauses[0].Holds != "proposed: not in force" { t.Fatalf("get after changes: %+v", g) } // Not judged again until it is revised. if said := w.reviewAll(t); len(said) != 0 || len(w.judge.reviews) != 1 { t.Fatalf("judged again unrevised: %v", said) } p := valid(n) p.Sections[2].Text = "Merges land sooner; nobody waits longer than ninety seconds." if _, err := w.k.Propose("reviewer@novox", p); err == nil || !strings.Contains(err.Error(), "only its author") { t.Fatalf("a revision by someone else: %v", err) } w.judge.review = "" if _, err := w.k.Propose("author@g14", p); err != nil { t.Fatal(err) } w.reviewAll(t) e = mustGet(t, s, Key{KindDecision, n}) if e.Current.Status != StatusAccepted || len(e.Current.Reviews) != 2 || e.Current.Reviews[1].Verdict != VerdictApprove || e.Current.Reviews[1].Version != 4 || e.Current.Reviews[0].Version != 2 { t.Fatalf("after the revision: %s, %+v", e.Current.Status, e.Current.Reviews) } }) } // A judgement that cannot be had — the judge unreachable, or its answer unreadable — leaves the record proposed, // says so, and is never taken as an approval. func TestAJudgementThatCannotBeHadNeverApproves(t *testing.T) { for _, c := range []struct{ name, answer string }{ {"unreachable", ""}, {"not json", `the record looks fine to me`}, {"no verdict", `{"findings":[]}`}, {"another verdict", `{"verdict":"approved","findings":[]}`}, {"a verdict in capitals", `{"verdict":"APPROVE","findings":[]}`}, {"changes naming none", `{"verdict":"changes","findings":[" "]}`}, } { t.Run(c.name, func(t *testing.T) { w := imported(t, newMemStore()) n := proposed(t, w, "author@g14", false) w.judge.review, w.judge.reviewFail = c.answer, c.answer == "" for try := 1; try <= MostReviewTries; try++ { if said := w.reviewAll(t); len(said) != 1 || !strings.Contains(said[0], "not reviewed") { t.Fatalf("try %d: %v", try, said) } e := mustGet(t, w.k.Store, Key{KindDecision, n}) if e.Current.Status != StatusProposed || e.Current.Judging == nil || e.Current.Judging.State != JudgingFailed || e.Current.Judging.Tries != try || len(e.Current.Reviews) != 0 { t.Fatalf("try %d: %s, %+v", try, e.Current.Status, e.Current.Judging) } g, _ := w.k.Get(GetAsk{Number: n}) if !strings.Contains(g.Review, "not reviewed") || !strings.Contains(g.Review, "counts as an approval") { t.Fatalf("get does not say it: %q", g.Review) } // Not asked again at once: after a pause that grows with each try. if said := w.reviewAll(t); len(said) != 0 { t.Fatalf("asked again at once: %v", said) } w.clock = w.clock.Add(time.Duration(try) * 10 * time.Minute) } // Tried enough: it stays proposed, saying its author proposes it again. w.clock = w.clock.Add(24 * time.Hour) if said := w.reviewAll(t); len(said) != 0 { t.Fatalf("asked after its last try: %v", said) } e := mustGet(t, w.k.Store, Key{KindDecision, n}) g, _ := w.k.Get(GetAsk{Number: n}) if e.Current.Status != StatusProposed || !strings.Contains(g.Review, "proposes it again") || len(w.said.of(EventAccepted)) != 0 { t.Fatalf("after the last try: %s, %q", e.Current.Status, g.Review) } h, _ := w.k.Holds("", &Key{KindDecision, n}, 0) if len(h.Clauses) != 0 { t.Fatalf("a record never reviewed holds %v", h.Clauses) } // Its author proposes it again, and that version is reviewed. w.judge.review, w.judge.reviewFail = "", false w.accepted(t, "author@g14", valid(n)) }) } } // A record revised while its judgement was asked is not decided on the version judged: the newest is judged next. func TestARecordRevisedWhileItIsJudgedIsJudgedAgain(t *testing.T) { w := imported(t, newMemStore()) n := proposed(t, w, "author@g14", false) p := valid(n) p.Sections[2].Text = "Merges land sooner, and the walk is shorter." w.judge.during = func() { if _, err := w.k.Propose("author@g14", p); err != nil { t.Error(err) } } said := w.reviewAll(t) if len(said) != 2 || !strings.Contains(said[0], "changed while it was judged") || len(w.judge.reviews) != 2 { t.Fatalf("%v", said) } e := mustGet(t, w.k.Store, Key{KindDecision, n}) if e.Current.Status != StatusAccepted || e.Current.Section("Consequences") != p.Sections[2].Text || e.Current.Reviews[0].Version != 3 { t.Fatalf("%s %+v", e.Current.Status, e.Current.Reviews) } } // A record with a version written while the runtime proved no caller is never reviewed, and so never accepted: // who wrote it cannot be told, and only its proven author revises it. Its author proposes it again. func TestARecordProposedUnprovenIsNeverReviewed(t *testing.T) { w := imported(t, newMemStore()) n := w.reserved(t, Unproven) got, err := w.k.Propose(Unproven, valid(n)) if err != nil { t.Fatal(err) } if !strings.Contains(got.Says, "proposes it again under a new number") { t.Fatalf("propose says %q", got.Says) } if said := w.reviewAll(t); len(said) != 0 || len(w.judge.reviews) != 0 { t.Fatalf("an unproven proposal was judged: %v", said) } g, _ := w.k.Get(GetAsk{Number: n}) if g.Status != StatusProposed || !strings.Contains(g.Review, "proved no caller") { t.Fatalf("%+v", g) } } // A record that supersedes another moves it and every clause of it to superseded. func TestASupersedingRecordSupersedesTheOlderOnAcceptance(t *testing.T) { w := imported(t, newMemStore()) n := w.reserved(t, "author@g14") p := valid(n) p.Links = []LinkAsk{{Kind: LinkSupersedes, To: "0276"}} p.Sections[0].Text = "The merge window is replaced." w.accepted(t, "author@g14", p) old := mustGet(t, w.k.Store, Key{KindDecision, 276}).Current if old.Status != StatusSuperseded || old.Clause(4).Status != ClauseSuperseded || old.Extra["superseded-by"] != itoaKey(n) { t.Fatalf("0276: %s, §4 %+v", old.Status, old.Clause(4)) } h, _ := w.k.Holds("", &Key{KindDecision, 276}, 0) if len(h.Clauses) != 0 { t.Fatalf("a superseded record holds %v", h.Clauses) } } func TestARecordThatNeedsTheOperatorIsAcceptedOnlyOnTheWarrant(t *testing.T) { eachStore(t, func(t *testing.T, s Store) { w := imported(t, s) n := proposed(t, w, "author@g14", true) w.reviewAll(t) if e := mustGet(t, s, Key{KindDecision, n}); e.Current.Status != StatusProposed || len(w.router.asked) != 1 { t.Fatalf("after the review: %s, asks %d", e.Current.Status, len(w.router.asked)) } ask := w.router.asked[0] if err := ask.Check(w.clock); err != nil || ask.Options[0].Level != asks.Approve || ask.Options[1].Level != asks.Approve || !strings.Contains(ask.Details, "the knowledge base's own review") || ask.Headline != "Merges land after ninety seconds of quiet" || asks.Glance(ask.Explanation) != nil || strings.Contains(ask.Explanation, "The window closes") { t.Fatalf("the ask: %v %+v", err, ask) } // Without the warrant: nothing is accepted, however often the router's record is read or the reviews run. for i := 0; i < 2; i++ { if _, err := w.k.Reconcile(); err != nil { t.Fatal(err) } w.reviewAll(t) } if e := mustGet(t, s, Key{KindDecision, n}); e.Current.Status != StatusProposed || len(w.judge.reviews) != 1 { t.Fatalf("accepted with no warrant: %s", e.Current.Status) } h, _ := w.k.Holds("", &Key{KindDecision, n}, 0) if len(h.Clauses) != 0 { t.Fatalf("a record waiting for the operator holds %v", h.Clauses) } // The operator's Approve, on a channel that proves who answered: accepted, the warrant kept. w.clock = w.clock.Add(10 * time.Minute) warrant := w.router.answer(ask.ID, OptionApprove, w.clock) said, err := w.k.Reconcile() if err != nil { t.Fatal(err) } e := mustGet(t, s, Key{KindDecision, n}) if e.Current.Status != StatusAccepted || e.Current.Ask == nil || e.Current.Ask.Warrant != warrant.Says() || e.Current.Ask.Said != AskApproved || len(said) != 1 { t.Fatalf("on the warrant: %+v, ask %+v, said %v", e.Current.Status, e.Current.Ask, said) } vs, _ := s.Versions(context.Background(), Key{KindDecision, n}) if last := vs[len(vs)-1]; !strings.Contains(last.Why, "warrant") || last.By != "the operator" { t.Fatalf("the accepting version: %+v", last) } if said, _ := w.k.Reconcile(); len(said) != 0 { t.Fatalf("decided twice: %v", said) } }) } // An ask that cannot be published is taken back: the record stays proposed and is reviewed again later. func TestAnAskThatCannotBePublishedLeavesTheRecordProposed(t *testing.T) { w := imported(t, newMemStore()) w.k.Asks = nil n := proposed(t, w, "author@g14", true) said := w.reviewAll(t) e := mustGet(t, w.k.Store, Key{KindDecision, n}) if len(said) != 1 || !strings.Contains(said[0], "cannot be asked") || e.Current.Status != StatusProposed || e.Current.Ask != nil || e.Current.Judging == nil || e.Current.Judging.State != JudgingFailed { t.Fatalf("%v: %s %+v", said, e.Current.Status, e.Current.Judging) } } // A warrant for an ask whose record changed after it was asked does not hold: the act it bound is another version. func TestAWarrantForAnotherVersionDoesNotAccept(t *testing.T) { w := imported(t, newMemStore()) n := proposed(t, w, "author@g14", true) w.reviewAll(t) // The record is changed behind the ask, keeping the ask: what the operator was shown is not what is held. ctx := context.Background() e := mustGet(t, w.k.Store, Key{KindDecision, n}) r := e.Current.Clone() r.Clauses[1].Text = "**A batch is delivered by many walks.** One commit per repository." if err := w.k.Store.Save(ctx, Version{By: "someone", Why: "changed behind the ask", Record: r}, true, false, e.Revision); err != nil { t.Fatal(err) } w.router.answer(w.router.asked[0].ID, OptionApprove, w.clock.Add(time.Minute)) if _, err := w.k.Reconcile(); err != nil { t.Fatal(err) } e = mustGet(t, w.k.Store, Key{KindDecision, n}) if e.Current.Status != StatusProposed || !strings.Contains(e.Current.Ask.Said, "does not hold") { t.Fatalf("accepted on a warrant for another version: %s, %+v", e.Current.Status, e.Current.Ask) } } func TestTheOperatorsDeclineLeavesTheRecordProposed(t *testing.T) { w := imported(t, newMemStore()) n := proposed(t, w, "author@g14", true) w.reviewAll(t) w.router.answer(w.router.asked[0].ID, OptionDecline, w.clock.Add(time.Minute)) if _, err := w.k.Reconcile(); err != nil { t.Fatal(err) } e := mustGet(t, w.k.Store, Key{KindDecision, n}) if e.Current.Status != StatusProposed || e.Current.Ask.Said != "declined by the operator" { t.Fatalf("%s %+v", e.Current.Status, e.Current.Ask) } } // An accepted record changes only by a progressive insight, which is reviewed like a proposal while the accepted // version is served. func TestAnAcceptedRecordChangesOnlyByAnInsightReviewedLikeAProposal(t *testing.T) { w := imported(t, newMemStore()) n := w.reserved(t, "author@g14") w.accepted(t, "author@g14", valid(n)) p := valid(n) p.Sections[2].Text = "Merges land sooner, by about a minute." if _, err := w.k.Propose("author@g14", p); err == nil || !strings.Contains(err.Error(), "progressive insight") { t.Fatalf("an accepted record rewritten: %v", err) } p.Insight = "it said only that merges land sooner" w.judge.review = `{"verdict":"changes","findings":["Consequences: say what a minute is measured from"]}` if _, err := w.k.Propose("author@g14", p); err != nil { t.Fatal(err) } w.reviewAll(t) e := mustGet(t, w.k.Store, Key{KindDecision, n}) if e.Pending == nil || e.Current.Section("Consequences") != "Merges land sooner." || len(e.Pending.Reviews) != 1 || e.Pending.Reviews[0].Verdict != VerdictChanges { t.Fatalf("the insight was served before its review approved it: %+v", e.Current.Sections) } w.judge.review = "" p.Sections[2].Text = "Merges land sooner, by about a minute from the last merge." if _, err := w.k.Propose("author@g14", p); err != nil { t.Fatal(err) } w.reviewAll(t) e = mustGet(t, w.k.Store, Key{KindDecision, n}) if e.Pending != nil || e.Current.Status != StatusAccepted || len(e.Current.Insights) != 1 || e.Current.Section("Consequences") != p.Sections[2].Text { t.Fatalf("after the insight's review: %+v", e.Current) } // 0276 §1 was changed once, by the acceptance; the insight keeps the link and changes it no further. if c := mustGet(t, w.k.Store, Key{KindDecision, 276}).Current.Clause(1); len(c.ChangedBy) != 1 { t.Fatalf("0276 §1 changed again by an insight: %v", c.ChangedBy) } } // No verb takes a review from anyone: the review is the knowledge base's own. func TestNoVerbTakesAReview(t *testing.T) { for _, tool := range Tools(&KB{}, callerOf) { if strings.HasSuffix(tool.Name, ".review") || strings.Contains(tool.Name, "review") { t.Errorf("%s is served", tool.Name) } } for _, v := range Verbs { if v == "review" { t.Error("review is a verb") } } } func TestReserveAsksTheTrackerAndANumberNeverProposedIsWithdrawn(t *testing.T) { w := newWorld(t, newMemStore(), fixture()) r, err := w.k.ReserveNumber("author@g14", KindResearch, "how a knowledge base is asked") if err != nil { t.Fatal(err) } if r.Number != 50 || len(w.tracker.asked) != 1 || w.tracker.asked[0]["kind"] != KindResearch || w.tracker.asked[0]["for"] != "how a knowledge base is asked" { t.Fatalf("%+v %v", r, w.tracker.asked) } if _, err := w.k.ReserveNumber("author@g14", "design", "x"); err == nil { t.Fatal("a design number reserved") } w.clock = w.clock.Add(DefaultReservedFor + time.Hour) if n, err := w.k.Withdraw(); err != nil || n != 1 { t.Fatalf("withdrew %d (%v)", n, err) } e := mustGet(t, w.k.Store, Key{KindResearch, 50}) if e.Current.Status != StatusWithdrawn || e.Current.ReservedBy != "author@g14" { t.Fatalf("%+v", e.Current) } if _, err := w.k.Propose("author@g14", ProposeAsk{Kind: KindResearch, Number: 50, Title: "late", Status: StatusActive, Sections: []Section{{Name: "00-overview.md", Text: "x"}}}); err == nil { t.Fatal("a withdrawn number took a proposal") } }