mesh/merge-gate pass: builds mesh-knowledge → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery failed: its walk failed, carrying its own commit: a gate on a first machine (what it carried put back), a build, a machine
The operator's channel edits a cleared message with the resolved line it was raised with, so one sent only with the clear was never shown.
200 lines
8.3 KiB
Go
200 lines
8.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// novox/hq issue 497: a review that could not be had is owed. What is owed is kept in the knowledge base's store,
|
|
// with its try count and when its next try is due, so the background work takes it up after a restart of the tool
|
|
// runner as it would have without one; the history says when it is asked again, so nobody takes a pause for a
|
|
// loss; and after its last try the history says so and the operator is told once. It is never an approval.
|
|
func TestAReviewOwedOutlivesARestartAndItsLastTryIsSaid(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, s Store) {
|
|
inZone(t)
|
|
w := imported(t, s)
|
|
n := proposed(t, w, "author@g14", false)
|
|
key := Key{KindDecision, n}
|
|
lastWhy := func() string {
|
|
vs, err := s.Versions(context.Background(), key)
|
|
if err != nil || len(vs) == 0 {
|
|
t.Fatalf("versions of %s: %v", key, err)
|
|
}
|
|
return vs[len(vs)-1].Why
|
|
}
|
|
var due time.Time
|
|
for try := 1; try <= MostReviewTries; try++ {
|
|
if try > 1 {
|
|
// The tool runner restarts: a new knowledge base over the same store, nothing of the old one's memory.
|
|
w = newWorld(t, s, fixture())
|
|
w.clock = due.Add(-time.Second)
|
|
if said := w.reviewAll(t); len(said) != 0 {
|
|
t.Fatalf("try %d asked before it was due: %v", try, said)
|
|
}
|
|
w.clock = due
|
|
}
|
|
w.judge.reviewFail = true
|
|
if said := w.reviewAll(t); len(said) != 1 || !strings.Contains(said[0], "not reviewed") {
|
|
t.Fatalf("try %d after a restart was not taken up: %v", try, said)
|
|
}
|
|
e := mustGet(t, s, key)
|
|
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 is not counted in the store: %s, %+v", try, e.Current.Status, e.Current.Judging)
|
|
}
|
|
why := lastWhy()
|
|
if !strings.Contains(why, fmt.Sprintf("try %d of %d", try, MostReviewTries)) {
|
|
t.Fatalf("try %d: the history does not count it: %q", try, why)
|
|
}
|
|
if try < MostReviewTries {
|
|
due = w.clock.Add(time.Duration(try) * 10 * time.Minute)
|
|
when := "asked again at " + due.Local().Format("2006-01-02 15:04 MST")
|
|
if utc := due.UTC().Format("15:04"); strings.Contains(when, utc) || !strings.Contains(when, "CEST") {
|
|
t.Fatalf("the time is not said locally: %q (UTC %s)", when, utc)
|
|
}
|
|
if !strings.Contains(why, when) {
|
|
t.Fatalf("try %d: the history does not say when it is asked again (%q): %q", try, when, why)
|
|
}
|
|
if g, _ := w.k.Get(GetAsk{Number: n}); !strings.Contains(g.Review, when) {
|
|
t.Fatalf("try %d: get does not say when it is asked again (%q): %q", try, when, g.Review)
|
|
}
|
|
if len(w.told) != 0 {
|
|
t.Fatalf("try %d: the operator was told before the last try: %v", try, w.told)
|
|
}
|
|
}
|
|
}
|
|
// After its last try: the history says it was the last, and the operator is told, once.
|
|
if why := lastWhy(); !strings.Contains(why, "the last") || !strings.Contains(why, "the operator is told") {
|
|
t.Fatalf("the last try is not said in the history: %q", why)
|
|
}
|
|
if len(w.told) != 1 || !strings.Contains(w.told[0], key.String()) || !strings.Contains(w.told[0], "never reviewed") {
|
|
t.Fatalf("the operator is not told once of the last try: %v", w.told)
|
|
}
|
|
w.clock = w.clock.Add(24 * time.Hour)
|
|
if said := w.reviewAll(t); len(said) != 0 || len(w.told) != 1 {
|
|
t.Fatalf("after the last try: asked %v, told %v", said, w.told)
|
|
}
|
|
// Another restart neither asks it again nor tells the operator again.
|
|
clock := w.clock
|
|
w = newWorld(t, s, fixture())
|
|
w.clock = clock
|
|
if said := w.reviewAll(t); len(said) != 0 || len(w.told) != 0 {
|
|
t.Fatalf("after a restart past the last try: asked %v, told %v", said, w.told)
|
|
}
|
|
// Never an approval.
|
|
if e := mustGet(t, s, key); e.Current.Status != StatusProposed || len(w.said.of(EventAccepted)) != 0 {
|
|
t.Fatalf("a review never had approved it: %s", e.Current.Status)
|
|
}
|
|
})
|
|
}
|
|
|
|
// novox/hq issue 497: the notice of a proposal never reviewed says it stays proposed, so it is cleared once that is
|
|
// no longer so; and a later version's run of tries is a notice of its own, told once whatever else is written.
|
|
func TestProposedAgainAfterTheLastTryClearsTheNotice(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, s Store) {
|
|
w := imported(t, s)
|
|
n := proposed(t, w, "author@g14", false)
|
|
key := Key{KindDecision, n}
|
|
runOut := func(from int) {
|
|
t.Helper()
|
|
w.judge.reviewFail = true
|
|
for try := from; try <= MostReviewTries; try++ {
|
|
if said := w.reviewAll(t); len(said) != 1 {
|
|
t.Fatalf("try %d: %v", try, said)
|
|
}
|
|
w.clock = w.clock.Add(time.Duration(try) * ReviewPause)
|
|
}
|
|
}
|
|
runOut(1)
|
|
e := mustGet(t, s, key)
|
|
first := fmt.Sprintf("knowledge.unreviewed.decision-%d.v%d", n, e.CurrentVersion)
|
|
if len(w.toldKeys) != 1 || w.toldKeys[0] != first || len(w.cleared) != 0 ||
|
|
w.resolved[0] != key.String()+" no longer waits for its review" {
|
|
t.Fatalf("after the last try: told %v, cleared %v", w.toldKeys, w.cleared)
|
|
}
|
|
// Another version written while it waits (as a link change on it writes one) tells nothing again.
|
|
r := e.Current.Clone()
|
|
if err := s.Save(context.Background(), Version{By: "test", At: w.clock, Why: "a link changed", Record: r}, true, false, e.Revision); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if w.reviewAll(t); len(w.toldKeys) != 1 || len(w.cleared) != 0 {
|
|
t.Fatalf("a version written while it waits: told %v, cleared %v", w.toldKeys, w.cleared)
|
|
}
|
|
// Proposed again: the notice is cleared, saying so.
|
|
if _, err := w.k.Propose("author@g14", valid(n)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.clock = w.clock.Add(time.Second)
|
|
w.reviewAll(t) // its try 1, which fails, and the clearing
|
|
w.clock = w.clock.Add(ReviewPause)
|
|
if len(w.cleared) != 1 || w.cleared[0][0] != first || !strings.Contains(w.cleared[0][1], "proposed again") {
|
|
t.Fatalf("proposed again: cleared %v", w.cleared)
|
|
}
|
|
// Its own run of tries out is a notice of its own, under its own version.
|
|
runOut(2)
|
|
if len(w.toldKeys) != 2 || w.toldKeys[1] == first || len(w.cleared) != 1 {
|
|
t.Fatalf("a later version ran out: told %v, cleared %v", w.toldKeys, w.cleared)
|
|
}
|
|
// Proposed again and accepted at last: that notice is cleared too, and nothing is cleared twice, also after
|
|
// a restart.
|
|
w.judge.reviewFail = false
|
|
w.accepted(t, "author@g14", valid(n))
|
|
if len(w.cleared) != 2 || w.cleared[1][0] != w.toldKeys[1] || !strings.Contains(w.cleared[1][1], "proposed again") {
|
|
t.Fatalf("accepted: cleared %v", w.cleared)
|
|
}
|
|
clock := w.clock
|
|
w = newWorld(t, s, fixture())
|
|
w.clock = clock
|
|
if w.reviewAll(t); len(w.cleared) != 0 || len(w.told) != 0 {
|
|
t.Fatalf("after a restart: told %v, cleared %v", w.told, w.cleared)
|
|
}
|
|
})
|
|
}
|
|
|
|
// inZone sets the local zone to one that is not UTC for one test, so a time said locally is proven to be, wherever
|
|
// the test runs.
|
|
func inZone(t *testing.T) {
|
|
t.Helper()
|
|
was := time.Local
|
|
time.Local = time.FixedZone("CEST", 2*60*60)
|
|
t.Cleanup(func() { time.Local = was })
|
|
}
|
|
|
|
// novox/hq issue 497: a clear the operator's channel refused keeps its notice as told, and is sent again at the next
|
|
// step until it is taken.
|
|
func TestAClearThatFailsIsSentAgain(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, s Store) {
|
|
w := imported(t, s)
|
|
n := proposed(t, w, "author@g14", false)
|
|
w.judge.reviewFail = true
|
|
for try := 1; try <= MostReviewTries; try++ {
|
|
w.reviewAll(t)
|
|
w.clock = w.clock.Add(time.Duration(try) * ReviewPause)
|
|
}
|
|
if len(w.toldKeys) != 1 {
|
|
t.Fatalf("after the last try: told %v", w.toldKeys)
|
|
}
|
|
prefix := fmt.Sprintf("knowledge.unreviewed.decision-%d.", n)
|
|
refused := 0
|
|
w.k.Clear = func(key, resolved string) error { refused++; return errors.New("the channel is down") }
|
|
if _, err := w.k.Propose("author@g14", valid(n)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.reviewAll(t)
|
|
if told, err := s.ToldOf(context.Background(), prefix); err != nil || len(told) != 1 || refused == 0 {
|
|
t.Fatalf("a refused clear: told %v (%v), refused %d", told, err, refused)
|
|
}
|
|
w.k.Clear = func(key, resolved string) error { w.cleared = append(w.cleared, [2]string{key, resolved}); return nil }
|
|
w.clock = w.clock.Add(time.Second)
|
|
w.reviewAll(t)
|
|
if told, err := s.ToldOf(context.Background(), prefix); err != nil || len(told) != 0 || len(w.cleared) != 1 ||
|
|
w.cleared[0][0] != w.toldKeys[0] {
|
|
t.Fatalf("the next step: told %v (%v), cleared %v", told, err, w.cleared)
|
|
}
|
|
})
|
|
}
|