From bb343bd44db80b0f2947ac782eb31e2ed099f521 Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 13 Oct 2026 17:05:23 +0000 Subject: [PATCH] watcher: a stop during the first scan sends no startup notification (closes #271) Run sent the startup notification right after the first scan without checking the context. RunOnce returns as usual when shutdown cuts the scan short, so a stop during the first scan sent "has started and completed its initial scan" to every endpoint as the process shut down. Run now sends it only when the context was not cancelled. The new test cancels the context before Run starts, so the scan never sends its lookup; a stop part way through a scan follows the same path. Model: opus-5-5 --- TODO.md | 2 ++ internal/watcher/cancelled_test.go | 24 ++++++++++++++++++++++++ internal/watcher/watcher.go | 7 ++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 0de395d..855cb8f 100644 --- a/TODO.md +++ b/TODO.md @@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149 # Completed Steps +- 2026-10-13: a stop before the first scan completes sends no startup + notification; a scan that ran to the end still sends it (closes #271). - 2026-10-13: a nameserver leaving a domain's delegation sends only NS Change; one leaving a hostname's zone sends NS Change, not NS Failure (closes #262). - 2026-10-13: `DNSWATCHER_METRICS_USERNAME` set without diff --git a/internal/watcher/cancelled_test.go b/internal/watcher/cancelled_test.go index 5d83bc1..247ab7c 100644 --- a/internal/watcher/cancelled_test.go +++ b/internal/watcher/cancelled_test.go @@ -83,6 +83,30 @@ func TestCancelledCheckSavesNothing(t *testing.T) { } } +// TestStopBeforeFirstScanSendsNoStartupNotification runs a watcher with +// the startup notification on and its context already cancelled, so the +// first scan is cut short before its lookup is sent. The scan did not +// complete, so no startup notification may be sent. +func TestStopBeforeFirstScanSendsNoStartupNotification(t *testing.T) { + t.Parallel() + + cfg := defaultTestConfig(t) + cfg.Hostnames = []string{testHost} + cfg.SendTestNotification = true + + w, deps := newTestWatcher(t, cfg) + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + w.Run(ctx) + + notifications := deps.notifier.getNotifications() + if len(notifications) != 0 { + t.Errorf("sent %v, want no notifications", notifications) + } +} + // newLoggingWatcher returns a watcher for a domain and a hostname, with // the real resolver, that writes what it logs at warning level or above // into the returned buffer. diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index 0748ee1..f7346f9 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -133,7 +133,12 @@ func (w *Watcher) Run(ctx context.Context) { w.cleanupRemovedTargets() w.RunOnce(ctx) - w.maybeSendTestNotification(ctx) + + // RunOnce returns as usual when shutdown cuts the scan short, and + // the startup notification says the scan completed. + if ctx.Err() == nil { + w.maybeSendTestNotification(ctx) + } dnsTicker := time.NewTicker(w.config.DNSInterval) tlsTicker := time.NewTicker(w.config.TLSInterval) -- 2.54.0