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) } }