Merge pull request 'Tests no longer race the wall clock; the checker invokes compose-candidate again' (#143) from fix/timing-tests into main

This commit was merged in pull request #143.
This commit is contained in:
2026-11-02 22:17:53 +00:00
18 changed files with 176 additions and 125 deletions
@@ -53,38 +53,23 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
if len(g.Asks()) != 0 { if len(g.Asks()) != 0 {
t.Fatal("a wait put an ask") t.Fatal("a wait put an ask")
} }
// Issue 1930: the bound is on a step that stands still, not on its age. A step that enters a new phase inside the bound is // Issue 1930: the bound is on a step that stands still, not on its age. A step past one bound from its start that entered a
// waited for until it ends; one that stays in a phase the bound fails at once, naming the phase; one that never stops moving // new phase inside the bound is waited for until it ends; one that stood a whole bound in a phase fails at once, naming
// ends at three bounds, said as such. // the phase; one three bounds from its start ends, said as kept moving. The steps' own times stand for the clock, so the
k = Asker{Publisher: g, Waiter: g, Bound: 60 * time.Millisecond} // bound is an hour no loaded machine reaches and no case waits on a pause (the check of 2026-11-02 22:35 tripped one
started := time.Now() // of 60 ms).
go func() { k = Asker{Publisher: g, Waiter: g, Bound: time.Hour}
for i := range 8 { now := time.Now()
g.Write(delivery.Step{ID: "mvl0frv18d1980", Node: "ace", State: delivery.StepRunning, Started: started, Phase: "p" + string(rune('a'+i)), PhaseAt: time.Now()}) g.Write(delivery.Step{ID: "mvl0frv18d1980", Node: "ace", State: delivery.StepRunning, Started: now.Add(-90 * time.Minute), Phase: "pb", PhaseAt: now})
time.Sleep(15 * time.Millisecond) g.Write(delivery.Step{ID: "mvl0frv18d1980", Node: "ace", State: delivery.StepPassed})
}
g.Write(delivery.Step{ID: "mvl0frv18d1980", Node: "ace", State: delivery.StepPassed})
}()
if step, err := k.Wait(context.Background(), goAsk("mvl0frv18d1980")); err != nil || step.State != delivery.StepPassed { if step, err := k.Wait(context.Background(), goAsk("mvl0frv18d1980")); err != nil || step.State != delivery.StepPassed {
t.Fatalf("a step that kept moving past its bound: %+v, %v", step, err) t.Fatalf("a step that kept moving past its bound: %+v, %v", step, err)
} }
g.Write(delivery.Step{ID: "mvl0frv18d1981", Node: "ace", State: delivery.StepRunning, Started: time.Now(), Phase: "integration: tests", PhaseAt: time.Now()}) g.Write(delivery.Step{ID: "mvl0frv18d1981", Node: "ace", State: delivery.StepRunning, Started: now.Add(-time.Hour), Phase: "integration: tests", PhaseAt: now.Add(-time.Hour)})
if _, err := k.Wait(context.Background(), goAsk("mvl0frv18d1981")); err == nil || !strings.Contains(err.Error(), `stood still in "integration: tests"`) { if _, err := k.Wait(context.Background(), goAsk("mvl0frv18d1981")); err == nil || !strings.Contains(err.Error(), `stood still in "integration: tests"`) {
t.Fatalf("a step that stood still in a phase: %v", err) t.Fatalf("a step that stood still in a phase: %v", err)
} }
loop := make(chan struct{}) g.Write(delivery.Step{ID: "mvl0frv18d1982", Node: "ace", State: delivery.StepRunning, Started: now.Add(-3 * time.Hour), Phase: "qz", PhaseAt: now})
defer close(loop)
started = time.Now()
go func() {
for i := 0; ; i++ {
select {
case <-loop:
return
case <-time.After(20 * time.Millisecond):
g.Write(delivery.Step{ID: "mvl0frv18d1982", Node: "ace", State: delivery.StepRunning, Started: started, Phase: "q" + string(rune('a'+i%26)), PhaseAt: time.Now()})
}
}
}()
if _, err := k.Wait(context.Background(), goAsk("mvl0frv18d1982")); err == nil || !strings.Contains(err.Error(), "kept moving") { if _, err := k.Wait(context.Background(), goAsk("mvl0frv18d1982")); err == nil || !strings.Contains(err.Error(), "kept moving") {
t.Fatalf("a step that never stops moving: %v", err) t.Fatalf("a step that never stops moving: %v", err)
} }
@@ -184,22 +169,21 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
} }
// Review (issue 734 path): an ask taken again by the same node (its build module replaced mid-run) is bounded from the // Review (issue 734 path): an ask taken again by the same node (its build module replaced mid-run) is bounded from the
// new run's start, so its pass is answered after the first run's bound would have ended the wait. // new run's start, not the first run's. The steps' own times stand for the clock: the first run started now, the second a
// whole bound ago, so the wait ends at once on the second run's bound where the first run's would wait an hour; no case
// waits on a pause a loaded machine can outlast (the check of 2026-11-02 22:35 tripped one of 60 ms). The context's
// bound is reached only by a wait that kept the first run's start.
func TestARedeliveryToTheSameNodeRestartsTheBound(t *testing.T) { func TestARedeliveryToTheSameNodeRestartsTheBound(t *testing.T) {
f := &FakeAsks{} f := &FakeAsks{}
k := Asker{Publisher: f, Waiter: f, Bound: 60 * time.Millisecond} k := Asker{Publisher: f, Waiter: f, Bound: time.Hour}
const id = "mvl0frv18d1962" const id = "mvl0frv18d1962"
first := time.Now() now := time.Now()
f.Write(delivery.Step{ID: id, Node: "ace", State: delivery.StepRunning, Started: first}) f.Write(delivery.Step{ID: id, Node: "ace", State: delivery.StepRunning, Started: now})
go func() { f.Write(delivery.Step{ID: id, Node: "ace", State: delivery.StepRunning, Started: now.Add(-time.Hour)})
time.Sleep(15 * time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
f.Write(delivery.Step{ID: id, Node: "ace", State: delivery.StepRunning, Started: time.Now()}) defer cancel()
time.Sleep(50 * time.Millisecond) // past the first run's bound, inside the second's if _, err := k.Wait(ctx, goAsk(id)); err == nil || !strings.Contains(err.Error(), "on ace did not end within 1h0m0s of its start") {
f.Write(delivery.Step{ID: id, Node: "ace", State: delivery.StepPassed, Started: time.Now()}) t.Fatalf("a redelivered run a bound old: %v, want its own bound to end the wait", err)
}()
step, err := k.Wait(context.Background(), goAsk(id))
if err != nil || step.State != delivery.StepPassed {
t.Fatalf("step %+v, %v, want the redelivered run's pass", step, err)
} }
} }
@@ -318,7 +318,7 @@ func (b *Builder) answer(ctx context.Context, l *lane, ask delivery.StepAsk, ask
// Not an asking: a node that sits out would use them all while it idles and pulls the ask again at once. // Not an asking: a node that sits out would use them all while it idles and pulls the ask again at once.
// Bounded by time from the first decline, then failed loudly naming the nodes and their reasons (as the // Bounded by time from the first decline, then failed loudly naming the nodes and their reasons (as the
// checker's decline loop is, issue 890). // checker's decline loop is, issue 890).
now := time.Now() now := b.now()
if firstDecline.IsZero() { if firstDecline.IsZero() {
firstDecline = now firstDecline = now
} }
@@ -75,7 +75,8 @@ type Builder struct {
Said func(AskedAgain) Said func(AskedAgain)
// Log is told each build's start and end, the entries of the builder's log (novox/hq ADR 0571 §1). Nil tells nobody. // Log is told each build's start and end, the entries of the builder's log (novox/hq ADR 0571 §1). Nil tells nobody.
Log func(delivery.BuildLogEntry) Log func(delivery.BuildLogEntry)
// Now is the clock of the log; nil is time.Now. Node is the node the builder runs on, which asked the build. // Now is the builder's clock: of the log, and of the decline bound (DeclineBound runs from the first decline); nil is
// time.Now. Node is the node the builder runs on, which asked the build.
Now func() time.Time Now func() time.Time
Node string Node string
@@ -318,7 +318,7 @@ func TestATrunkFingerprintIsOneAskOnTheDeliveryStream(t *testing.T) {
} }
r.b.Module(context.Background(), "m", data) r.b.Module(context.Background(), "m", data)
var asks []delivery.StepAsk var asks []delivery.StepAsk
for deadline := time.Now().Add(2 * time.Second); len(asks) == 0; time.Sleep(time.Millisecond) { for deadline := time.Now().Add(10 * time.Second); len(asks) == 0; time.Sleep(time.Millisecond) {
if time.Now().After(deadline) { if time.Now().After(deadline) {
t.Fatal("no ask was put") t.Fatal("no ask was put")
} }
@@ -705,7 +705,7 @@ func (g *goneAsks) asked() int {
// waitForAsks waits until n asks were put. // waitForAsks waits until n asks were put.
func waitForAsks(t *testing.T, r *rig, n int) []delivery.StepAsk { func waitForAsks(t *testing.T, r *rig, n int) []delivery.StepAsk {
t.Helper() t.Helper()
for deadline := time.Now().Add(2 * time.Second); len(r.asks.Asks()) < n; time.Sleep(time.Millisecond) { for deadline := time.Now().Add(10 * time.Second); len(r.asks.Asks()) < n; time.Sleep(time.Millisecond) {
if time.Now().After(deadline) { if time.Now().After(deadline) {
t.Fatalf("%d asks put, want %d", len(r.asks.Asks()), n) t.Fatalf("%d asks put, want %d", len(r.asks.Asks()), n)
} }
@@ -725,7 +725,13 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
r.b.Gone, r.b.CheckAfter = gone, 5*time.Millisecond r.b.Gone, r.b.CheckAfter = gone, 5*time.Millisecond
r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) r.b.Module(context.Background(), "m", moduleValue(t, "modules/m"))
waitForAsks(t, r, 1) waitForAsks(t, r, 1)
time.Sleep(100 * time.Millisecond) // Five check bounds passed (five reads of whether it can be answered), so the ask waited past the step bound: an event
// to wait on, where a pause of 100 ms said nothing on a loaded machine.
for deadline := time.Now().Add(10 * time.Second); gone.asked() < 5; time.Sleep(time.Millisecond) {
if time.Now().After(deadline) {
t.Fatalf("whether the ask can still be answered was read %d times within 10 s, want 5", gone.asked())
}
}
if v := valueOf(t, r.st, r.key); v.State != delivery.BuildBuilding { if v := valueOf(t, r.st, r.key); v.State != delivery.BuildBuilding {
t.Fatalf("the key = %+v while its ask waits, want building", v) t.Fatalf("the key = %+v while its ask waits, want building", v)
} }
@@ -741,11 +747,16 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
t.Run("declined by a node off the agreed toolchain: put again, no asking used, the bound ends it loudly", func(t *testing.T) { // ADR 0584 clause 2 t.Run("declined by a node off the agreed toolchain: put again, no asking used, the bound ends it loudly", func(t *testing.T) { // ADR 0584 clause 2
r := newRig(t, goBundle) r := newRig(t, goBundle)
r.asks.Answer = nil r.asks.Answer = nil
r.b.DeclinePause, r.b.DeclineBound = -1, 150*time.Millisecond // The builder's clock steps a second at each read, so the bound of a minute ends the declines after many asks however
// slow the machine (a bound of 150 ms of wall clock let a loaded one put fewer than DefaultAskings).
var clock atomic.Int64
start := time.Now()
r.b.Now = func() time.Time { return start.Add(time.Duration(clock.Add(1)) * time.Second) }
r.b.DeclinePause, r.b.DeclineBound = -1, time.Minute
r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) r.b.Module(context.Background(), "m", moduleValue(t, "modules/m"))
declined, _ := json.Marshal(delivery.StepDeclined{Declined: true, Lacks: []string{"the agreed toolchain go1.27.1 (recipe 1)"}}) declined, _ := json.Marshal(delivery.StepDeclined{Declined: true, Lacks: []string{"the agreed toolchain go1.27.1 (recipe 1)"}})
waitForAsks(t, r, 1) waitForAsks(t, r, 1)
for ended, deadline := 0, time.Now().Add(5*time.Second); valueOf(t, r.st, r.key).State == delivery.BuildBuilding && time.Now().Before(deadline); time.Sleep(5 * time.Millisecond) { for ended, deadline := 0, time.Now().Add(20*time.Second); valueOf(t, r.st, r.key).State == delivery.BuildBuilding && time.Now().Before(deadline); time.Sleep(5 * time.Millisecond) {
if asks := r.asks.Asks(); len(asks) > ended { // each ask put again is declined in its turn if asks := r.asks.Asks(); len(asks) > ended { // each ask put again is declined in its turn
r.asks.End(delivery.Step{ID: asks[ended].ID, Node: "ace", State: delivery.StepRefused, Reason: "this node builds with go1.26.0", Result: declined}) r.asks.End(delivery.Step{ID: asks[ended].ID, Node: "ace", State: delivery.StepRefused, Reason: "this node builds with go1.26.0", Result: declined})
ended++ ended++
@@ -926,7 +937,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
} }
if c.byHand { if c.byHand {
waitForAsks(t, r, 1) waitForAsks(t, r, 1)
for deadline := time.Now().Add(2 * time.Second); len(r.b.AskAgain(r.key, "stuck since 19:06")) == 0; time.Sleep(time.Millisecond) { for deadline := time.Now().Add(10 * time.Second); len(r.b.AskAgain(r.key, "stuck since 19:06")) == 0; time.Sleep(time.Millisecond) {
if time.Now().After(deadline) { if time.Now().After(deadline) {
t.Fatal("no lane of the key waits") t.Fatal("no lane of the key waits")
} }
@@ -934,7 +945,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
} }
if c.deaf { if c.deaf {
r.asks.End(r.answer(waitForAsks(t, r, 1)[0])) r.asks.End(r.answer(waitForAsks(t, r, 1)[0]))
for deadline := time.Now().Add(2 * time.Second); heard.Waiting() == 0; time.Sleep(time.Millisecond) { for deadline := time.Now().Add(10 * time.Second); heard.Waiting() == 0; time.Sleep(time.Millisecond) {
if time.Now().After(deadline) { if time.Now().After(deadline) {
t.Fatal("no ask is waited on") t.Fatal("no ask is waited on")
} }
@@ -7,7 +7,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
@@ -261,6 +260,26 @@ type bed struct {
srvURL string srvURL string
announce []string announce []string
mu sync.Mutex mu sync.Mutex
logs saidLog
}
// saidLog is what the program said, safe for its goroutines and the test's.
type saidLog struct {
mu sync.Mutex
b strings.Builder
}
func (l *saidLog) Write(p []byte) (int, error) { l.mu.Lock(); defer l.mu.Unlock(); return l.b.Write(p) }
func (l *saidLog) String() string { l.mu.Lock(); defer l.mu.Unlock(); return l.b.String() }
// said waits until the program said words, within 20 s: an event to wait on, not a pause a loaded machine can outlast.
func (b *bed) said(t *testing.T, words string) {
t.Helper()
for deadline := time.Now().Add(20 * time.Second); !strings.Contains(b.logs.String(), words); time.Sleep(10 * time.Millisecond) {
if time.Now().After(deadline) {
t.Fatalf("the checker did not say %q within 20 s:\n%s", words, b.logs.String())
}
}
} }
// engine places each node and serves compose-candidate for the first one, as its node-engine does on the bus (answered on // engine places each node and serves compose-candidate for the first one, as its node-engine does on the bus (answered on
@@ -512,7 +531,7 @@ func (b *bed) start(t *testing.T, env map[string]string) {
} }
runCtx, stop := context.WithCancel(b.ctx) runCtx, stop := context.WithCancel(b.ctx)
done := make(chan error, 1) done := make(chan error, 1)
go func() { done <- run(runCtx, func(k string) string { return e[k] }, io.Discard) }() go func() { done <- run(runCtx, func(k string) string { return e[k] }, &b.logs) }()
t.Cleanup(func() { stop(); <-done }) t.Cleanup(func() { stop(); <-done })
} }
@@ -639,12 +658,13 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128
t.Fatal(err) t.Fatal(err)
} }
tk, _ := delivery.TurnsKey(repo) tk, _ := delivery.TurnsKey(repo)
if _, err := b.buckets[delivery.TurnsBucket].Put(b.ctx, tk, turn); err != nil { turnRev, err := b.buckets[delivery.TurnsBucket].Put(b.ctx, tk, turn)
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(300 * time.Millisecond) // the checker hears the turn b.said(t, fmt.Sprintf("merge turn %s changed (revision %d)", tk, turnRev)) // the checker heard the turn
third := b.candidate(t, "feat/z", pull(repo, 9, "3333333333333333333333333333333333333333")) third := b.candidate(t, "feat/z", pull(repo, 9, "3333333333333333333333333333333333333333"))
time.Sleep(time.Second) b.said(t, third+": not claimed: the approved merge of fix/other") // it held the check off
if _, err := b.buckets[delivery.CheckerVerdictsBucket].Get(b.ctx, third); !errors.Is(err, jetstream.ErrKeyNotFound) { if _, err := b.buckets[delivery.CheckerVerdictsBucket].Get(b.ctx, third); !errors.Is(err, jetstream.ErrKeyNotFound) {
t.Fatalf("verdicts.%s exists (%v) while a merge turn holds its repository", third, err) t.Fatalf("verdicts.%s exists (%v) while a merge turn holds its repository", third, err)
} }
@@ -303,6 +303,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
go func() { go func() {
errs <- follow(ctx, tw, func(e jetstream.KeyValueEntry) { errs <- follow(ctx, tw, func(e jetstream.KeyValueEntry) {
if turns.Put(e.Key(), e.Value(), !opOf(e)) { if turns.Put(e.Key(), e.Value(), !opOf(e)) {
// Said, so a wait on a held check can be read (and a test waits on it rather than on a pause).
logger.Printf("the gate's merge turn %s changed (revision %d): the checks it holds are held or claimed from now on", e.Key(), e.Revision())
ch.Reconsider(ctx) // a turn went or changed: a check it held is claimed now ch.Reconsider(ctx) // a turn went or changed: a check it held is claimed now
} }
}) })
@@ -33,10 +33,11 @@ func turnKey(t testing.TB, repo string) string {
return k return k
} }
// waitFor polls a condition the checker's goroutines make true, failing after two seconds. // waitFor polls a condition the checker's goroutines make true, failing after ten seconds: a bound a loaded machine does
// not reach, waited out only by a failing test.
func waitFor(t testing.TB, what string, ok func() bool) { func waitFor(t testing.TB, what string, ok func() bool) {
t.Helper() t.Helper()
for deadline := time.Now().Add(2 * time.Second); !ok(); time.Sleep(time.Millisecond) { for deadline := time.Now().Add(10 * time.Second); !ok(); time.Sleep(time.Millisecond) {
if time.Now().After(deadline) { if time.Now().After(deadline) {
t.Fatalf("%s did not happen", what) t.Fatalf("%s did not happen", what)
} }
@@ -957,7 +957,7 @@ func TestACheckAskWaitingInItsStreamIsNotBounded(t *testing.T) {
b.ex.Asks = buildrun.Asker{Publisher: b.asks, Waiter: b.asks, Bound: 20 * time.Millisecond} b.ex.Asks = buildrun.Asker{Publisher: b.asks, Waiter: b.asks, Bound: 20 * time.Millisecond}
done := make(chan Outcome, 1) done := make(chan Outcome, 1)
go func() { done <- b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) }() go func() { done <- b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) }()
for deadline := time.Now().Add(2 * time.Second); len(b.asks.Asks()) == 0; time.Sleep(time.Millisecond) { for deadline := time.Now().Add(10 * time.Second); len(b.asks.Asks()) == 0; time.Sleep(time.Millisecond) {
if time.Now().After(deadline) { if time.Now().After(deadline) {
t.Fatal("no ask was put") t.Fatal("no ask was put")
} }
@@ -82,10 +82,13 @@ func TestAnAskGivenUpOrNeverTakenEndsItsTryAsErrorAndItsHeadIsAskedAnew(t *testi
name string name string
lose int lose int
never bool never bool
want string // notTaken is the try's bound on an ask no node takes: short only where it is what ends the try, an hour where the
// dead letter must (a loaded machine let 200 ms pass before the dead letter was heard).
notTaken time.Duration
want string
}{ }{
{"given up by the build seat", 1, false, buildrun.LostWords + " after 5 deliveries at 2026-10-26T12:26:40Z"}, {"given up by the build seat", 1, false, time.Hour, buildrun.LostWords + " after 5 deliveries at 2026-10-26T12:26:40Z"},
{"taken by no build node within the bound", 0, true, buildrun.NotTakenWords + " within 200ms"}, {"taken by no build node within the bound", 0, true, 200 * time.Millisecond, buildrun.NotTakenWords + " within 200ms"},
} { } {
t.Run(row.name, func(t *testing.T) { t.Run(row.name, func(t *testing.T) {
kv := newMemKV() kv := newMemKV()
@@ -97,7 +100,7 @@ func TestAnAskGivenUpOrNeverTakenEndsItsTryAsErrorAndItsHeadIsAskedAnew(t *testi
b := newBench(t) b := newBench(t)
l := losing(b, row.lose) l := losing(b, row.lose)
l.never = row.never l.never = row.never
tr := &Tryer{Tries: kv, Judge: b.ex, NotTaken: 200 * time.Millisecond} tr := &Tryer{Tries: kv, Judge: b.ex, NotTaken: row.notTaken}
raw, _, _, _ := kv.Get(ctx, id) raw, _, _, _ := kv.Get(ctx, id)
tr.Put(ctx, id, raw) tr.Put(ctx, id, raw)
tr.Wait() tr.Wait()
@@ -119,7 +122,7 @@ func TestAnAskGivenUpOrNeverTakenEndsItsTryAsErrorAndItsHeadIsAskedAnew(t *testi
tr := &Tryer{Tries: kv, Judge: b.ex} // no bound: only the hand ends it tr := &Tryer{Tries: kv, Judge: b.ex} // no bound: only the hand ends it
raw, _, _, _ := kv.Get(ctx, id) raw, _, _, _ := kv.Get(ctx, id)
tr.Put(ctx, id, raw) tr.Put(ctx, id, raw)
deadline := time.Now().Add(2 * time.Second) deadline := time.Now().Add(10 * time.Second)
for readTry(t, kv, id).State != TryRunning && time.Now().Before(deadline) { for readTry(t, kv, id).State != TryRunning && time.Now().Before(deadline) {
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
} }
@@ -289,7 +292,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
key := keyOf(t, "feat/x", cand) key := keyOf(t, "feat/x", cand)
r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Candidate(ctx, "feat%2Fx", cand)
if row.deaf > 0 { if row.deaf > 0 {
for deadline := time.Now().Add(2 * time.Second); heard.Waiting() == 0 && time.Now().Before(deadline); { for deadline := time.Now().Add(10 * time.Second); heard.Waiting() == 0 && time.Now().Before(deadline); {
time.Sleep(2 * time.Millisecond) time.Sleep(2 * time.Millisecond)
} }
if v := r.verdict(t, key); v.State != delivery.VerdictRunning { if v := r.verdict(t, key); v.State != delivery.VerdictRunning {
@@ -111,13 +111,11 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
} }
// 1887/A5b: the checker reads nothing of the controller's plans-building (ADR 0483 superseded by ADR 0525 §7), calls no // 1887/A5b: the checker reads nothing of the controller's plans-building (ADR 0483 superseded by ADR 0525 §7), calls no
// build module's tool and holds no capacity: it puts check asks on the Go and Node build seats (ADR 0525 §1, §4). // build module's tool and holds no capacity: it puts check asks on the Go and Node build seats (ADR 0525 §1, §4).
// The forge's reads for a mesh-sdk pin (ADR 0484 clause 9). Not yet the node seat node-engine's compose-candidate // The forge's reads for a mesh-sdk pin (ADR 0484 clause 9), and of the node seat node-engine its one verb
// (ADR 0593 §7): the indexer that runs while this delivers was built before the verb existed and refuses a manifest // compose-candidate, asked of every placed node (ADR 0593 §7), never the seat's other verbs.
// invoking it, which held the grants part's whole list (issue 1126, 2026-11-02); a follow-up adds it back once the if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches",
// new indexer runs. "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", t.Errorf("invokes = %v, want the forge's reads and compose-candidate", m.Invokes)
"gitea.gitea_list_branches"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
t.Errorf("invokes = %v, want the forge's reads", m.Invokes)
} }
if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) { if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
t.Errorf("uses = %v, want the Go and Node build seats", m.Uses) t.Errorf("uses = %v, want the Go and Node build seats", m.Uses)
+2 -1
View File
@@ -65,7 +65,8 @@
"gitea.gitea_commits", "gitea.gitea_commits",
"gitea.gitea_compare", "gitea.gitea_compare",
"gitea.gitea_get_file", "gitea.gitea_get_file",
"gitea.gitea_list_branches" "gitea.gitea_list_branches",
"seat:node-engine.compose-candidate"
], ],
"own-secrets": { "own-secrets": {
@@ -308,14 +308,14 @@ func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2
t.Fatal(err) t.Fatal(err)
} }
var info *jetstream.StreamInfo var info *jetstream.StreamInfo
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if info, err = forge.Info(ctx); err == nil && info.State.Msgs >= 2 { if info, err = forge.Info(ctx); err == nil && info.State.Msgs >= 2 {
break break
} }
} }
// The open message the bridge writes at its start (ADR 0443 §5), and the event's message. // The open message the bridge writes at its start (ADR 0443 §5), and the event's message.
if info.State.Msgs != 2 { if info.State.Msgs != 2 {
t.Fatalf("FORGE holds %d messages within 5 s, want 2", info.State.Msgs) t.Fatalf("FORGE holds %d messages within 20 s, want 2", info.State.Msgs)
} }
raw, err := forge.GetLastMsgForSubject(ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.pull") raw, err := forge.GetLastMsgForSubject(ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.pull")
if err != nil { if err != nil {
@@ -336,7 +336,7 @@ func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
ci, err := cons.Info(ctx) ci, err := cons.Info(ctx)
if err == nil && ci.NumAckPending == 0 && ci.NumPending == 0 && ci.Delivered.Consumer >= 1 { if err == nil && ci.NumAckPending == 0 && ci.NumPending == 0 && ci.Delivered.Consumer >= 1 {
return return
@@ -403,14 +403,14 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5
t.Fatal(err) t.Fatal(err)
} }
var info *jetstream.StreamInfo var info *jetstream.StreamInfo
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if info, err = forge.Info(ctx); err == nil && info.State.Msgs >= 2 { if info, err = forge.Info(ctx); err == nil && info.State.Msgs >= 2 {
break break
} }
} }
// The open message the bridge writes at its start (ADR 0443 §5), and the event's message. // The open message the bridge writes at its start (ADR 0443 §5), and the event's message.
if info.State.Msgs != 2 { if info.State.Msgs != 2 {
t.Fatalf("FORGE holds %d messages within 5 s, want 2", info.State.Msgs) t.Fatalf("FORGE holds %d messages within 20 s, want 2", info.State.Msgs)
} }
raw, err := forge.GetLastMsgForSubject(ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.merged") raw, err := forge.GetLastMsgForSubject(ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.merged")
if err != nil { if err != nil {
@@ -443,7 +443,7 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
ci, err := cons.Info(ctx) ci, err := cons.Info(ctx)
if err == nil && ci.NumAckPending == 0 && ci.NumPending == 0 && ci.Delivered.Consumer >= 1 { if err == nil && ci.NumAckPending == 0 && ci.NumPending == 0 && ci.Delivered.Consumer >= 1 {
return return
@@ -465,13 +465,13 @@ func TestBridgeSaysTheOpenPullsAtStart(t *testing.T) { // 1237/AC1
t.Fatal(err) t.Fatal(err)
} }
var info *jetstream.StreamInfo var info *jetstream.StreamInfo
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if info, err = forge.Info(ctx); err == nil && info.State.Msgs >= 3 { if info, err = forge.Info(ctx); err == nil && info.State.Msgs >= 3 {
break break
} }
} }
if info.State.Msgs != 3 { if info.State.Msgs != 3 {
t.Fatalf("FORGE holds %d messages within 5 s, want 3", info.State.Msgs) t.Fatalf("FORGE holds %d messages within 20 s, want 3", info.State.Msgs)
} }
for _, repo := range []string{"mesh-delivery", "mesh-catalog"} { for _, repo := range []string{"mesh-delivery", "mesh-catalog"} {
raw, err := forge.GetLastMsgForSubject(ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox."+repo+".pull") raw, err := forge.GetLastMsgForSubject(ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox."+repo+".pull")
@@ -65,7 +65,7 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10
hash, _ := delivery.HeadsHash([]delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: h1}, {Repository: "novox/mesh-catalog", Number: 300, Head: h2}}) hash, _ := delivery.HeadsHash([]delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: h1}, {Repository: "novox/mesh-catalog", Number: 300, Head: h2}})
var c delivery.Candidate var c delivery.Candidate
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
e, err := candidates.Get(ctx, "feat/x") e, err := candidates.Get(ctx, "feat/x")
if err != nil { if err != nil {
continue continue
@@ -75,7 +75,7 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10
} }
} }
if c.Group != "feat/x" || c.Base != 0 || c.HeadsHash != hash || len(c.Pulls) != 2 { if c.Group != "feat/x" || c.Base != 0 || c.HeadsHash != hash || len(c.Pulls) != 2 {
t.Fatalf("candidate = %+v within 5 s", c) t.Fatalf("candidate = %+v within 20 s", c)
} }
for _, p := range c.Pulls { for _, p := range c.Pulls {
want := map[string]string{"novox/mesh-delivery": "a", "novox/mesh-catalog": "c"}[p.Repository] want := map[string]string{"novox/mesh-delivery": "a", "novox/mesh-catalog": "c"}[p.Repository]
@@ -195,7 +195,7 @@ func (l buildLog) end(t *testing.T, repo, module, path string) {
} }
// TestIndexerWritesModulesAtAMerge: a module's record and its newest pointer are in mesh-controller_builds; a merged // TestIndexerWritesModulesAtAMerge: a module's record and its newest pointer are in mesh-controller_builds; a merged
// message on FORGE makes modules.<m> within 5 s, the fingerprint of the merge's tree and the recorded inputs. // message on FORGE makes modules.<m> within 20 s, the fingerprint of the merge's tree and the recorded inputs.
func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the modules are the merged message's, no build record names them (issue 1851) func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the modules are the merged message's, no build record names them (issue 1851)
// The bucket holds only build-agent's record, which no fingerprint reads since R4g (ADR 0530 §1). // The bucket holds only build-agent's record, which no fingerprint reads since R4g (ADR 0530 §1).
js, ctx, _, modules := startIndexer(t, func(ctx context.Context, builds buildLog) { js, ctx, _, modules := startIndexer(t, func(ctx context.Context, builds buildLog) {
@@ -212,7 +212,7 @@ func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the m
t.Fatal(err) t.Fatal(err)
} }
var m delivery.Module var m delivery.Module
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, "a"); err == nil { if e, err := modules.Get(ctx, "a"); err == nil {
if m, err = delivery.DecodeModule(e.Value()); err == nil { if m, err = delivery.DecodeModule(e.Value()); err == nil {
break break
@@ -220,12 +220,12 @@ func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the m
} }
} }
if m.Repository != "novox/mesh-delivery" || m.Commit != merge || m.Path != "modules/a" || m.Manifest == "" || m.FP != "" || m.None != "" { // 1606/AC2 if m.Repository != "novox/mesh-delivery" || m.Commit != merge || m.Path != "modules/a" || m.Manifest == "" || m.FP != "" || m.None != "" { // 1606/AC2
t.Errorf("module = %+v within 5 s, want path modules/a, its manifest, and no value of today's form", m) t.Errorf("module = %+v within 20 s, want path modules/a, its manifest, and no value of today's form", m)
} }
} }
// TestIndexerKeepsManifestsAtAMerge: a merged message of mesh-catalog carrying the real manifest of the module avahi and the // TestIndexerKeepsManifestsAtAMerge: a merged message of mesh-catalog carrying the real manifest of the module avahi and the
// hash of its go.sum (the bridge's Manifests and Locks, issue 1448) makes, within 5 s, modules.avahi with its manifest digest, // hash of its go.sum (the bridge's Manifests and Locks, issue 1448) makes, within 20 s, modules.avahi with its manifest digest,
// its inputs and the reason it has no fingerprint of the new form, and manifests.<digest> with the manifest byte for byte. // its inputs and the reason it has no fingerprint of the new form, and manifests.<digest> with the manifest byte for byte.
// //
// And it judges them (novox/hq ADR 0578, issue 1585): with three manifests more, one valid, one refused and one with a // And it judges them (novox/hq ADR 0578, issue 1585): with three manifests more, one valid, one refused and one with a
@@ -272,7 +272,7 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
t.Fatal(err) t.Fatal(err)
} }
var m delivery.Module var m delivery.Module
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, "avahi"); err == nil { if e, err := modules.Get(ctx, "avahi"); err == nil {
if m, err = delivery.DecodeModule(e.Value()); err == nil { if m, err = delivery.DecodeModule(e.Value()); err == nil {
break break
@@ -282,7 +282,7 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
digest := delivery.ManifestDigest(manifest) digest := delivery.ManifestDigest(manifest)
if m.Manifest != digest || m.None2 != "toolchain not yet reported" || m.FP2 != "" || m.Inputs2 == nil || m.Inputs2.Tree != "newtree" || if m.Manifest != digest || m.None2 != "toolchain not yet reported" || m.FP2 != "" || m.Inputs2 == nil || m.Inputs2.Tree != "newtree" ||
m.Inputs2.Locks["tools"] != goSum || m.Path != "modules/avahi" { // 1606/AC2 m.Inputs2.Locks["tools"] != goSum || m.Path != "modules/avahi" { // 1606/AC2
t.Errorf("module = %+v within 5 s, want its path modules/avahi, its manifest digest %s, its inputs and none2 'toolchain not yet reported'", m, digest) t.Errorf("module = %+v within 20 s, want its path modules/avahi, its manifest digest %s, its inputs and none2 'toolchain not yet reported'", m, digest)
} }
kv, err := js.KeyValue(ctx, delivery.ManifestsBucket) kv, err := js.KeyValue(ctx, delivery.ManifestsBucket)
if err != nil { if err != nil {
@@ -345,7 +345,7 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
// TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain: the bucket mesh-build-go_toolchains exists (the state // TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain: the bucket mesh-build-go_toolchains exists (the state
// toolchains of the build module, delivery.ToolchainsBucket) and holds no key; a merged message of mesh-catalog carrying // toolchains of the build module, delivery.ToolchainsBucket) and holds no key; a merged message of mesh-catalog carrying
// avahi's real manifest makes modules.avahi with none2 'toolchain not yet reported'; the bucket made after the indexer started, a put of the key n1.go and the build module's event toolchain.reported (the build // avahi's real manifest makes modules.avahi with none2 'toolchain not yet reported'; the bucket made after the indexer started, a put of the key n1.go and the build module's event toolchain.reported (the build
// module's own layout, delivery.ToolchainKey and EncodeToolchain) makes, within 5 s and with no merge, its fingerprint: // module's own layout, delivery.ToolchainKey and EncodeToolchain) makes, within 20 s and with no merge, its fingerprint:
// no builder input is waited for (ADR 0530 §1; issue 1887 I1). // no builder input is waited for (ADR 0530 §1; issue 1887 I1).
func TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain(t *testing.T) { // 1444/AC10 func TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain(t *testing.T) { // 1444/AC10
manifest, err := os.ReadFile("../../internal/index/testdata/catalogue-a67f0196-avahi.module.json") manifest, err := os.ReadFile("../../internal/index/testdata/catalogue-a67f0196-avahi.module.json")
@@ -369,7 +369,7 @@ func TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain(t *tes
} }
none2 := func(want string) string { none2 := func(want string) string {
var got string var got string
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, "avahi"); err == nil { if e, err := modules.Get(ctx, "avahi"); err == nil {
if m, err := delivery.DecodeModule(e.Value()); err == nil { if m, err := delivery.DecodeModule(e.Value()); err == nil {
if got = m.None2; got == want { if got = m.None2; got == want {
@@ -381,7 +381,7 @@ func TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain(t *tes
return got return got
} }
if got := none2(delivery.ToolchainNoReport); got != delivery.ToolchainNoReport { if got := none2(delivery.ToolchainNoReport); got != delivery.ToolchainNoReport {
t.Fatalf("none2 = %q within 5 s, want %q", got, delivery.ToolchainNoReport) t.Fatalf("none2 = %q within 20 s, want %q", got, delivery.ToolchainNoReport)
} }
// The bucket is made only now, as when the build module is assigned after the indexer started (review finding 9): the // The bucket is made only now, as when the build module is assigned after the indexer started (review finding 9): the
// indexer finds it on its own. // indexer finds it on its own.
@@ -405,7 +405,7 @@ func TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain(t *tes
t.Fatal(err) t.Fatal(err)
} }
if got := none2(""); got != "" { if got := none2(""); got != "" {
t.Errorf("none2 = %q within 5 s of the toolchain key, want a fingerprint and no reason", got) t.Errorf("none2 = %q within 20 s of the toolchain key, want a fingerprint and no reason", got)
} }
} }
@@ -413,7 +413,7 @@ func TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain(t *tes
// and its durable consumer (made by the controller from the checker's declaration and the indexer's consumes) and no bucket // and its durable consumer (made by the controller from the checker's declaration and the indexer's consumes) and no bucket
// mesh-delivery-checker_previews; a pull request of mesh-catalog touching avahi makes the candidate with the reason // mesh-delivery-checker_previews; a pull request of mesh-catalog touching avahi makes the candidate with the reason
// 'no merge preview is computed'; the checker then makes its bucket, writes the preview of that head onto the trunk head // 'no merge preview is computed'; the checker then makes its bucket, writes the preview of that head onto the trunk head
// the source names, and says so on its stream; the candidate's avahi gets, within 5 s and with no restart, the reason // the source names, and says so on its stream; the candidate's avahi gets, within 20 s and with no restart, the reason
// computed from the preview (the toolchain is not reported here, so 'toolchain not yet reported'). // computed from the preview (the toolchain is not reported here, so 'toolchain not yet reported').
func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBucket(t *testing.T) { // 1445/AC4 func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBucket(t *testing.T) { // 1445/AC4
manifest, err := os.ReadFile("../../internal/index/testdata/catalogue-a67f0196-avahi.module.json") manifest, err := os.ReadFile("../../internal/index/testdata/catalogue-a67f0196-avahi.module.json")
@@ -453,7 +453,7 @@ func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBuc
} }
none2 := func(want string) string { none2 := func(want string) string {
var got string var got string
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := candidates.Get(ctx, "feat/x"); err == nil { if e, err := candidates.Get(ctx, "feat/x"); err == nil {
if c, err := delivery.DecodeCandidate(e.Value()); err == nil && len(c.Pulls) == 1 { if c, err := delivery.DecodeCandidate(e.Value()); err == nil && len(c.Pulls) == 1 {
got = c.Pulls[0].Fps["avahi"].None2 got = c.Pulls[0].Fps["avahi"].None2
@@ -466,7 +466,7 @@ func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBuc
return got return got
} }
if got := none2("no merge preview is computed"); got != "no merge preview is computed" { if got := none2("no merge preview is computed"); got != "no merge preview is computed" {
t.Fatalf("none2 = %q within 5 s of the pull request, want 'no merge preview is computed'", got) t.Fatalf("none2 = %q within 20 s of the pull request, want 'no merge preview is computed'", got)
} }
// The checker's bucket is made only now, as when the checker is assigned after the indexer started. // The checker's bucket is made only now, as when the checker is assigned after the indexer started.
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.PreviewsBucket, History: delivery.PreviewsHistory}) kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.PreviewsBucket, History: delivery.PreviewsHistory})
@@ -487,13 +487,13 @@ func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBuc
t.Fatal(err) t.Fatal(err)
} }
if got := none2(delivery.ToolchainNoReport); got != delivery.ToolchainNoReport { if got := none2(delivery.ToolchainNoReport); got != delivery.ToolchainNoReport {
t.Errorf("none2 = %q within 5 s of the preview, want %q (computed from the preview)", got, delivery.ToolchainNoReport) t.Errorf("none2 = %q within 20 s of the preview, want %q (computed from the preview)", got, delivery.ToolchainNoReport)
} }
} }
// TestIndexerJudgesAnUnreadableVerdictAgainAtStart: the modules bucket holds avahi as an older SDK wrote it, with the reason // TestIndexerJudgesAnUnreadableVerdictAgainAtStart: the modules bucket holds avahi as an older SDK wrote it, with the reason
// 'the manifest cannot be read', the inputs the merge carried (the tree, the lock hash by file name) and the digest of its // 'the manifest cannot be read', the inputs the merge carried (the tree, the lock hash by file name) and the digest of its
// manifest, which manifests.<digest> holds and the running SDK reads; the go toolchain is reported. Within 5 s of the start // manifest, which manifests.<digest> holds and the running SDK reads; the go toolchain is reported. Within 20 s of the start
// and with no repository merge the value carries a fingerprint and no reason (issue 1923). // and with no repository merge the value carries a fingerprint and no reason (issue 1923).
func TestIndexerJudgesAnUnreadableVerdictAgainAtStart(t *testing.T) { // 1923/AC3 func TestIndexerJudgesAnUnreadableVerdictAgainAtStart(t *testing.T) { // 1923/AC3
manifest, err := os.ReadFile("../../internal/index/testdata/catalogue-a67f0196-avahi.module.json") manifest, err := os.ReadFile("../../internal/index/testdata/catalogue-a67f0196-avahi.module.json")
@@ -548,7 +548,7 @@ func TestIndexerJudgesAnUnreadableVerdictAgainAtStart(t *testing.T) { // 1923/AC
} }
}) })
var got delivery.Module var got delivery.Module
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, "avahi"); err == nil { if e, err := modules.Get(ctx, "avahi"); err == nil {
if m, err := delivery.DecodeModule(e.Value()); err == nil { if m, err := delivery.DecodeModule(e.Value()); err == nil {
if got = m; got.FP2 != "" { if got = m; got.FP2 != "" {
@@ -558,7 +558,7 @@ func TestIndexerJudgesAnUnreadableVerdictAgainAtStart(t *testing.T) { // 1923/AC
} }
} }
if got.FP2 == "" || got.None2 != "" { if got.FP2 == "" || got.None2 != "" {
t.Errorf("avahi = fp2 %q none2 %q within 5 s of the start, want a fingerprint and no reason", got.FP2, got.None2) t.Errorf("avahi = fp2 %q none2 %q within 20 s of the start, want a fingerprint and no reason", got.FP2, got.None2)
} }
} }
@@ -626,7 +626,7 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
} }
read := func(ok func(delivery.Module) bool) delivery.Module { read := func(ok func(delivery.Module) bool) delivery.Module {
var got delivery.Module var got delivery.Module
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, "avahi"); err == nil { if e, err := modules.Get(ctx, "avahi"); err == nil {
if m, err := delivery.DecodeModule(e.Value()); err == nil { if m, err := delivery.DecodeModule(e.Value()); err == nil {
if got = m; ok(m) { if got = m; ok(m) {
@@ -640,7 +640,7 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
agreed := func(m delivery.Module) bool { return m.FP2 != "" && m.Toolchains["go"].Version == "go1.27.3" } agreed := func(m delivery.Module) bool { return m.FP2 != "" && m.Toolchains["go"].Version == "go1.27.3" }
held := read(agreed) held := read(agreed)
if !agreed(held) { if !agreed(held) {
t.Fatalf("within 5 s avahi has fp2 %q, none2 %q, toolchains %v: want a fingerprint of n1's go1.27.3 alone", held.FP2, held.None2, held.Toolchains) t.Fatalf("within 20 s avahi has fp2 %q, none2 %q, toolchains %v: want a fingerprint of n1's go1.27.3 alone", held.FP2, held.None2, held.Toolchains)
} }
place(ctx, t, "n2", "mesh-build-go") place(ctx, t, "n2", "mesh-build-go")
key, _ := delivery.ToolchainKey("n2", "go") key, _ := delivery.ToolchainKey("n2", "go")
@@ -670,10 +670,10 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
return m.FP2 == "" && strings.HasPrefix(m.None2, "builders disagree: go1.27.2 recipe 1 on n2") return m.FP2 == "" && strings.HasPrefix(m.None2, "builders disagree: go1.27.2 recipe 1 on n2")
} }
if m := read(disagree); !disagree(m) { if m := read(disagree); !disagree(m) {
t.Fatalf("within 5 s of a merge after n2's placement avahi has fp2 %q, none2 %q: want the builders to disagree", m.FP2, m.None2) t.Fatalf("within 20 s of a merge after n2's placement avahi has fp2 %q, none2 %q: want the builders to disagree", m.FP2, m.None2)
} }
place(ctx, t, "n2") place(ctx, t, "n2")
if m := read(agreed); !agreed(m) { if m := read(agreed); !agreed(m) {
t.Fatalf("within 5 s of n2's placement taken away avahi has fp2 %q, none2 %q", m.FP2, m.None2) t.Fatalf("within 20 s of n2's placement taken away avahi has fp2 %q, none2 %q", m.FP2, m.None2)
} }
} }
@@ -20,7 +20,7 @@ import (
// TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage (ADR 0593 §2-§4; D11): the indexer whole against the // TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage (ADR 0593 §2-§4; D11): the indexer whole against the
// bus. Two Go modules of the catalogue whose go.sum names mesh-sdk; their builds end reporting their imports (importer: // bus. Two Go modules of the catalogue whose go.sum names mesh-sdk; their builds end reporting their imports (importer:
// buildstep; plain: announce); then mesh-sdk merges a change of buildstep alone, which moves mesh-sdk's version. Within // buildstep; plain: announce); then mesh-sdk merges a change of buildstep alone, which moves mesh-sdk's version. Within
// 5 s the packages key of mesh-sdk/go names the merge with a digest per package, importer gets a new fingerprint, and // 20 s the packages key of mesh-sdk/go names the merge with a digest per package, importer gets a new fingerprint, and
// plain keeps the one it had, though the version line would have moved it. The forge's tools answer on the bus as the // plain keeps the one it had, though the version line would have moved it. The forge's tools answer on the bus as the
// gitea module's do. A merge of go/ alone before it writes no value of mesh-sdk, whose tree is its named paths'. // gitea module's do. A merge of go/ alone before it writes no value of mesh-sdk, whose tree is its named paths'.
func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T) { func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T) {
@@ -85,14 +85,14 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
module := func(name string, ok func(delivery.Module) bool) delivery.Module { module := func(name string, ok func(delivery.Module) bool) delivery.Module {
t.Helper() t.Helper()
var m delivery.Module var m delivery.Module
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, name); err == nil { if e, err := modules.Get(ctx, name); err == nil {
if m, err = delivery.DecodeModule(e.Value()); err == nil && ok(m) { if m, err = delivery.DecodeModule(e.Value()); err == nil && ok(m) {
return m return m
} }
} }
} }
t.Fatalf("modules.%s = %+v within 5 s", name, m) t.Fatalf("modules.%s = %+v within 20 s", name, m)
return m return m
} }
sdkObjects := func(src string) map[string]map[string]string { sdkObjects := func(src string) map[string]map[string]string {
@@ -161,7 +161,7 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
} }
key, _ := delivery.PackagesKey(sdkGo) key, _ := delivery.PackagesKey(sdkGo)
var pub delivery.PublishedPackages var pub delivery.PublishedPackages
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := packages.Get(ctx, key); err == nil { if e, err := packages.Get(ctx, key); err == nil {
if pub, err = delivery.DecodePublishedPackages(e.Value()); err == nil && pub.Commit == m2 { if pub, err = delivery.DecodePublishedPackages(e.Value()); err == nil && pub.Commit == m2 {
break break
@@ -169,7 +169,7 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
} }
} }
if pub.Commit != m2 || len(pub.Packages) != 3 { if pub.Commit != m2 || len(pub.Packages) != 3 {
t.Fatalf("packages.%s = %+v within 5 s, want the merge %s with a digest of go, announce and buildstep", key, pub, m2) t.Fatalf("packages.%s = %+v within 20 s, want the merge %s with a digest of go, announce and buildstep", key, pub, m2)
} }
moved := module("importer", func(m delivery.Module) bool { return m.FP2 != importer.FP2 }) moved := module("importer", func(m delivery.Module) bool { return m.FP2 != importer.FP2 })
if moved.Packages[sdkGo] != m2 { if moved.Packages[sdkGo] != m2 {
@@ -128,7 +128,7 @@ func TestABuildModulesBucketThatAppearsLaterIsWatchedWhenItsEventArrives(t *test
} }
// It is followed from now on: a key put later is applied, and a second report only recomputes. // It is followed from now on: a key put later is applied, and a second report only recomputes.
bucket.w.ch <- fakeEntry{key: "ace.go", value: value} bucket.w.ch <- fakeEntry{key: "ace.go", value: value}
for deadline := time.Now().Add(3 * time.Second); time.Now().Before(deadline); time.Sleep(5 * time.Millisecond) { for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); time.Sleep(5 * time.Millisecond) {
mu.Lock() mu.Lock()
n := len(applies) n := len(applies)
mu.Unlock() mu.Unlock()
@@ -20,6 +20,8 @@ import (
"git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/process"
"mesh-delivery-teller/internal/ends"
) )
// busConf is the shape of the composed accounts file (mesh-controller internal/broker): one account with JetStream, the // busConf is the shape of the composed accounts file (mesh-controller internal/broker): one account with JetStream, the
@@ -175,7 +177,9 @@ const (
) )
func fullConf(dir string) string { func fullConf(dir string) string {
teller := append(append(append(keepsOf(tellerAnswers), keepsOf(tellerAsked)...), readOf(gateAsksKV)...), // ends is the teller's own state (manifest state entry ends), which the controller makes before it starts: a bed
// without it held the start ten seconds on a read the bus never answered, and the tests raced that stall.
teller := append(append(append(append(keepsOf(tellerAnswers), keepsOf(tellerAsked)...), keepsOf(ends.Bucket)...), readOf(gateAsksKV)...),
seatAsk, seatCancel, seatCondition, "$JS.API.DIRECT.GET.KV_"+messengerAsks+".$KV."+messengerAsks+".mesh-delivery-teller.>") seatAsk, seatCancel, seatCondition, "$JS.API.DIRECT.GET.KV_"+messengerAsks+".$KV."+messengerAsks+".mesh-delivery-teller.>")
teller = append(append(teller, readOf(assignerAsks)...), readOf(viewAnswers)...) // issue 1297 B10: the manifest's reads teller = append(append(teller, readOf(assignerAsks)...), readOf(viewAnswers)...) // issue 1297 B10: the manifest's reads
view := append(append(append(readOf(tellerAnswers), readOf(gateAsksKV)...), readOf(assignerAsks)...), keepsOf(viewAnswers)...) view := append(append(append(readOf(tellerAnswers), readOf(gateAsksKV)...), readOf(assignerAsks)...), keepsOf(viewAnswers)...)
@@ -244,7 +248,7 @@ func startFull(t *testing.T) *fullBed {
} }
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
t.Cleanup(cancel) t.Cleanup(cancel)
for name, history := range map[string]uint8{gateAsksKV: delivery.AsksHistory, tellerAnswers: delivery.AnswersHistory, tellerAsked: 1, messengerAsks: 1, assignerAsks: delivery.AssignerAsksHistory, viewAnswers: delivery.ViewAnswersHistory} { for name, history := range map[string]uint8{gateAsksKV: delivery.AsksHistory, tellerAnswers: delivery.AnswersHistory, tellerAsked: 1, ends.Bucket: 1, messengerAsks: 1, assignerAsks: delivery.AssignerAsksHistory, viewAnswers: delivery.ViewAnswersHistory} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: history}); err != nil { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: history}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -256,6 +260,21 @@ func startFull(t *testing.T) *fullBed {
return &fullBed{srv: srv, ctl: ctl, js: js, ctx: ctx} return &fullBed{srv: srv, ctl: ctl, js: js, ctx: ctx}
} }
// listening waits until the program hears the router's warrants (its subscription on the decided subject is on the bus),
// which it does before it watches any bucket: an event, not a pause a loaded machine can outlast.
func (b *fullBed) listening(t *testing.T) {
t.Helper()
acc, err := b.srv.LookupAccount("MESH")
if err != nil {
t.Fatal(err)
}
for deadline := time.Now().Add(20 * time.Second); !acc.SubscriptionInterest(seatDecided); time.Sleep(10 * time.Millisecond) {
if time.Now().After(deadline) {
t.Fatal("the program did not subscribe to the router's warrants within 20 s")
}
}
}
func (b *fullBed) as(t *testing.T, name string) jetstream.JetStream { func (b *fullBed) as(t *testing.T, name string) jetstream.JetStream {
t.Helper() t.Helper()
c, err := nats.Connect(b.srv.ClientURL(), nats.UserInfo(name, "pw"), nats.CustomInboxPrefix("_INBOX."+name)) c, err := nats.Connect(b.srv.ClientURL(), nats.UserInfo(name, "pw"), nats.CustomInboxPrefix("_INBOX."+name))
@@ -318,7 +337,7 @@ func TestCredentialsWriteOnlyTheirOwnBuckets(t *testing.T) { // 1343/AC4
// warrant on the teller's decided subject, and the answer is under the ask's key in the teller's bucket; withdrawing a // warrant on the teller's decided subject, and the answer is under the ask's key in the teller's bucket; withdrawing a
// second ask cancels it (1343/AC1, AC2). // second ask cancels it (1343/AC1, AC2).
func TestTellerCarriesAnAskAndWritesTheAnswer(t *testing.T) { // 1343/AC1, AC2 func TestTellerCarriesAnAskAndWritesTheAnswer(t *testing.T) { // 1343/AC1, AC2
now = time.Now().UTC() t.Cleanup(nowIs(time.Now().UTC()))
b := startFull(t) b := startFull(t)
tools := b.as(t, "n1.node-tools") tools := b.as(t, "n1.node-tools")
broker := filepath.Join(t.TempDir(), "broker") broker := filepath.Join(t.TempDir(), "broker")
@@ -351,8 +370,8 @@ func TestTellerCarriesAnAskAndWritesTheAnswer(t *testing.T) { // 1343/AC1, AC2
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// The program hears warrants before it reads the asks; give it its subscription before the ask is written. // The program hears warrants before it reads the asks; it has its subscription before the ask is written.
time.Sleep(500 * time.Millisecond) b.listening(t)
if _, err := kv.Put(b.ctx, key, value); err != nil { if _, err := kv.Put(b.ctx, key, value); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -423,7 +442,7 @@ func TestTellerCarriesAnAskAndWritesTheAnswer(t *testing.T) { // 1343/AC1, AC2
// lapsed; the refused ask is on the seat's work queue again, which the bus's duplicate window would have dropped under its // lapsed; the refused ask is on the seat's work queue again, which the bus's duplicate window would have dropped under its
// first message id (1610/AC1 on the real bus). // first message id (1610/AC1 on the real bus).
func TestTellerAsksAgainAnAskRefusedForWantOfRoom(t *testing.T) { // 1610/AC1 func TestTellerAsksAgainAnAskRefusedForWantOfRoom(t *testing.T) { // 1610/AC1
now = time.Now().UTC() t.Cleanup(nowIs(time.Now().UTC()))
b := startFull(t) b := startFull(t)
tools := b.as(t, "n1.node-tools") tools := b.as(t, "n1.node-tools")
broker := filepath.Join(t.TempDir(), "broker") broker := filepath.Join(t.TempDir(), "broker")
@@ -443,7 +462,7 @@ func TestTellerAsksAgainAnAskRefusedForWantOfRoom(t *testing.T) { // 1610/AC1
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(500 * time.Millisecond) b.listening(t)
// lastAsk waits for the seat's queue to hold an ask newer than seq under id, and gives its sequence. // lastAsk waits for the seat's queue to hold an ask newer than seq under id, and gives its sequence.
lastAsk := func(id string, after uint64) uint64 { lastAsk := func(id string, after uint64) uint64 {
t.Helper() t.Helper()
@@ -495,7 +514,7 @@ func TestTellerAsksAgainAnAskRefusedForWantOfRoom(t *testing.T) { // 1610/AC1
// (rewriting the view's answer does not write a second), and a view answer under the merge's key fills nothing // (rewriting the view's answer does not write a second), and a view answer under the merge's key fills nothing
// (1297/B10 AC: a view answer fills the ask once; an ask needing proof is not answered from a view key). // (1297/B10 AC: a view answer fills the ask once; an ask needing proof is not answered from a view key).
func TestTellerFillsAnAskFromTheViewsAnswerOnce(t *testing.T) { // 1297/B10 AC func TestTellerFillsAnAskFromTheViewsAnswerOnce(t *testing.T) { // 1297/B10 AC
now = time.Now().UTC() t.Cleanup(nowIs(time.Now().UTC()))
b := startFull(t) b := startFull(t)
assigner, view := b.as(t, "n1.mesh-delivery-assigner"), b.as(t, "n1.mesh-delivery-view") assigner, view := b.as(t, "n1.mesh-delivery-assigner"), b.as(t, "n1.mesh-delivery-view")
broker := filepath.Join(t.TempDir(), "broker") broker := filepath.Join(t.TempDir(), "broker")
@@ -521,7 +540,7 @@ func TestTellerFillsAnAskFromTheViewsAnswerOnce(t *testing.T) { // 1297/B10 AC
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(500 * time.Millisecond) // the program is watching b.listening(t) // the program hears warrants and watches
if _, err := askKV.Put(b.ctx, key, value); err != nil { if _, err := askKV.Put(b.ctx, key, value); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -568,7 +587,7 @@ func TestTellerFillsAnAskFromTheViewsAnswerOnce(t *testing.T) { // 1297/B10 AC
// ask's key, where the assigner reads it; a second ask the assigner takes away (a hold it lifted) is cancelled at the router // ask's key, where the assigner reads it; a second ask the assigner takes away (a hold it lifted) is cancelled at the router
// (1906/H1, H2 on the real bus). // (1906/H1, H2 on the real bus).
func TestTellerRaisesAnAssignerAskAndWritesTheOperatorsAnswer(t *testing.T) { func TestTellerRaisesAnAssignerAskAndWritesTheOperatorsAnswer(t *testing.T) {
now = time.Now().UTC() t.Cleanup(nowIs(time.Now().UTC()))
b := startFull(t) b := startFull(t)
assigner := b.as(t, "n1.mesh-delivery-assigner") assigner := b.as(t, "n1.mesh-delivery-assigner")
broker := filepath.Join(t.TempDir(), "broker") broker := filepath.Join(t.TempDir(), "broker")
@@ -586,7 +605,7 @@ func TestTellerRaisesAnAssignerAskAndWritesTheOperatorsAnswer(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(500 * time.Millisecond) // the program hears warrants and watches b.listening(t) // the program hears warrants and watches
if _, err := askKV.Put(b.ctx, key, value); err != nil { if _, err := askKV.Put(b.ctx, key, value); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -662,7 +681,7 @@ func TestTellerRaisesAnAssignerAskAndWritesTheOperatorsAnswer(t *testing.T) {
// TestTellerTakesAChangedHoldAnswerFromTheView starts the program against the bus: a hold ask answered "keep" earlier, and the // TestTellerTakesAChangedHoldAnswerFromTheView starts the program against the bus: a hold ask answered "keep" earlier, and the
// view's answer "release" written now, replace the answer of that asking with "release" (1906/N23 on the real bus). // view's answer "release" written now, replace the answer of that asking with "release" (1906/N23 on the real bus).
func TestTellerTakesAChangedHoldAnswerFromTheView(t *testing.T) { func TestTellerTakesAChangedHoldAnswerFromTheView(t *testing.T) {
now = time.Now().UTC() t.Cleanup(nowIs(time.Now().UTC()))
b := startFull(t) b := startFull(t)
assigner, view := b.as(t, "n1.mesh-delivery-assigner"), b.as(t, "n1.mesh-delivery-view") assigner, view := b.as(t, "n1.mesh-delivery-assigner"), b.as(t, "n1.mesh-delivery-view")
broker := filepath.Join(t.TempDir(), "broker") broker := filepath.Join(t.TempDir(), "broker")
@@ -694,7 +713,7 @@ func TestTellerTakesAChangedHoldAnswerFromTheView(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(500 * time.Millisecond) // the program is watching b.listening(t) // the program hears warrants and watches
if _, err := viewKV.Put(b.ctx, key+".view", viewAnswer(t, "release", "it was a flake")); err != nil { if _, err := viewKV.Put(b.ctx, key+".view", viewAnswer(t, "release", "it was a flake")); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -25,6 +25,14 @@ const (
var now = time.Date(2026, 10, 20, 9, 0, 0, 0, time.UTC) var now = time.Date(2026, 10, 20, 9, 0, 0, 0, time.UTC)
// nowIs sets the tests' clock now to at and returns what puts it back; a test sets it only as t.Cleanup(nowIs(at)), so no
// test leaves its clock to the next one (-shuffle runs them in any order).
func nowIs(at time.Time) (restore func()) {
was := now
now = at
return func() { now = was }
}
// memStore is a state bucket in memory with the revisions and the compare-and-set of the runtime's state. // memStore is a state bucket in memory with the revisions and the compare-and-set of the runtime's state.
type memStore struct { type memStore struct {
entries map[string]*stdio.StateEntry entries map[string]*stdio.StateEntry
@@ -2,6 +2,7 @@ package watch
import ( import (
"strings" "strings"
"sync/atomic"
"testing" "testing"
"time" "time"
@@ -372,8 +373,8 @@ func TestTheWatchmansFindings(t *testing.T) {
expire(w, c, signalKey("S9", "bus.ace.web.slow-consumer"), AdvisoryQuiet) expire(w, c, signalKey("S9", "bus.ace.web.slow-consumer"), AdvisoryQuiet)
}, map[string]string{}}, }, map[string]string{}},
{"WM5: while letters are held DEAD_LETTERS is counted again at its own pulse's expiry, and at a letter's age", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { {"WM5: while letters are held DEAD_LETTERS is counted again at its own pulse's expiry, and at a letter's age", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
n := 0 var n atomic.Int32
w.SetRecount(func() { n++ }) w.SetRecount(func() { n.Add(1) })
w.DeadLettersHeld(map[string]int{"EVENTS.ace_web": 1}, func(s, c string) string { return c }) w.DeadLettersHeld(map[string]int{"EVENTS.ace_web": 1}, func(s, c string) string { return c })
if o.ttls[signalKey("dead", "recount")] != DeadRecountEvery || !strings.Contains(o.findings["bus.EVENTS.ace_web.max-deliveries"].Said, "every 5m0s") { if o.ttls[signalKey("dead", "recount")] != DeadRecountEvery || !strings.Contains(o.findings["bus.EVENTS.ace_web.max-deliveries"].Said, "every 5m0s") {
t.Fatalf("pulses %v findings %v", o.ttls, o.findings) t.Fatalf("pulses %v findings %v", o.ttls, o.findings)
@@ -381,9 +382,11 @@ func TestTheWatchmansFindings(t *testing.T) {
w.DeadAging("EVENTS.ace_web", c.t.Add(time.Minute)) w.DeadAging("EVENTS.ace_web", c.t.Add(time.Minute))
expire(w, c, signalKey("dead", "EVENTS.ace_web"), time.Minute) expire(w, c, signalKey("dead", "EVENTS.ace_web"), time.Minute)
expire(w, c, signalKey("dead", "recount"), DeadRecountEvery) expire(w, c, signalKey("dead", "recount"), DeadRecountEvery)
time.Sleep(10 * time.Millisecond) // The recount runs in a goroutine of its own: waited for, within seconds no loaded machine reaches.
if n != 2 { for deadline := time.Now().Add(10 * time.Second); n.Load() < 2 && time.Now().Before(deadline); time.Sleep(time.Millisecond) {
t.Fatalf("counted again %d times", n) }
if n.Load() != 2 {
t.Fatalf("counted again %d times", n.Load())
} }
w.DeadLettersHeld(map[string]int{"EVENTS.ace_web": 1}, func(s, c string) string { return c }) // the count renews its pulse w.DeadLettersHeld(map[string]int{"EVENTS.ace_web": 1}, func(s, c string) string { return c }) // the count renews its pulse
w.DeadLettersHeld(map[string]int{}, nil) w.DeadLettersHeld(map[string]int{}, nil)