Files
mesh-catalog/modules/mesh-mail-notifier/cmd/mesh-mail-notifier/run_test.go
T
jochen 31120ed580
mesh/merge-gate head 31120ed58029 merged into trunk c9848f4fb85c — pass: builds mesh-mail-notifier → g14, shanks; no bus step; every machine composes with …
mesh/repo-check head 31120ed58029 merged into trunk c9848f4fb85c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh-mail-notifier: watch retries a subscribe refusal instead of exiting (issue 859, review round 1)
A subscribe the runtime refuses no longer calls os.Exit(1), which took mail-notifier.status and
mail_notifier_health down with it: watch now retries with backoff (2s doubling to 1 minute), logging each
refusal, as mesh-knowledge's hear does. Also: the run() test proves liveness by having the watcher answer a
ping after run returns, instead of checking a channel only the test's own deferred cleanup could close; and
the manifest test is renamed to say what it checks (no process resource at all).
2026-10-15 23:17:14 +02:00

115 lines
3.7 KiB
Go

package main
import (
"errors"
"io"
"strings"
"sync/atomic"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// TestRunStartsTheWatcherEvenWhenStdinIsAtEOF proves the watcher runs beside stdio.Serve in the one process the
// node's runtime launches (novox/hq ADR 0378 §9, as mesh-knowledge's serve starts go hear(k) and go
// background(k) before stdio.Serve), not only in a separate process something keeps asking over stdin. run is
// given stdin already at EOF, exactly what systemd gave the old separate `notify` process: it exited at once,
// every five seconds (novox/hq issue 859). The watcher must still be running once run has returned — proved by
// making it actually answer after run returns, not by the absence of a signal that nothing but the test's own
// cleanup could ever raise.
func TestRunStartsTheWatcherEvenWhenStdinIsAtEOF(t *testing.T) {
started := make(chan struct{})
ping := make(chan struct{})
pong := make(chan struct{})
fakeWatcher := func(n *Notifier) {
close(started)
for range ping {
pong <- struct{}{}
}
}
n := &Notifier{Store: newFakeStore()}
n.init()
done := make(chan error, 1)
go func() { done <- run(n, fakeWatcher, strings.NewReader(""), io.Discard, nil) }()
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("the watcher was not started before stdio.Serve returned")
}
select {
case err := <-done:
if err != nil {
t.Fatalf("run over stdin at EOF: %v", err)
}
case <-time.After(time.Second):
t.Fatal("run did not return once stdin reached EOF, as systemd gives a process nobody talks to")
}
// run has returned (stdio.Serve hit EOF at once); the watcher must still be live and answering, not merely
// not yet torn down. A watcher that stopped when run returned would never take this ping.
select {
case ping <- struct{}{}:
case <-time.After(time.Second):
t.Fatal("the watcher is no longer running once run returned: it must not stop just because stdio.Serve did")
}
select {
case <-pong:
case <-time.After(time.Second):
t.Fatal("the watcher did not answer once run returned")
}
close(ping)
}
// TestWatchRetriesASubscribeRefusalWithBackoffAndNeverExits proves watch no longer exits the process on a
// subscribe the runtime refuses — the old os.Exit(1) took mail-notifier.status and mail_notifier_health down
// with it — but retries with backoff and logs each refusal, as modules/mesh-knowledge's hear does (novox/hq
// issue 859, review round 1). A subscribe that fails twice then succeeds must leave the process serving (run
// still returns normally over stdin at EOF, never os.Exit) and the watcher subscribed (its third, successful
// call).
func TestWatchRetriesASubscribeRefusalWithBackoffAndNeverExits(t *testing.T) {
var calls int32
subscribed := make(chan struct{})
fakeSubscribe := func(pattern string, handle func(stdio.Envelope) error) error {
if atomic.AddInt32(&calls, 1) <= 2 {
return errors.New("the runtime refused this subscribe")
}
close(subscribed)
return nil
}
noSleep := func(time.Duration) {}
n := &Notifier{Store: newFakeStore()}
n.init()
done := make(chan error, 1)
go func() {
done <- run(n, func(n *Notifier) { watchWith(n, fakeSubscribe, noSleep) },
strings.NewReader(""), io.Discard, nil)
}()
select {
case err := <-done:
if err != nil {
t.Fatalf("run over stdin at EOF: %v", err)
}
case <-time.After(time.Second):
t.Fatal("run did not return once stdin reached EOF: a subscribe refusal must not hold up or kill the process")
}
select {
case <-subscribed:
case <-time.After(time.Second):
t.Fatal("the watcher never subscribed after its first two refusals")
}
if got := atomic.LoadInt32(&calls); got != 3 {
t.Fatalf("subscribe was called %d time(s), want 3 (two refusals, then the one that succeeded)", got)
}
}