carlos / platform Public

Report SNS as available only once its client is built

A valid CARLOS_ALERT_SNS_TOPIC_ARN with no recorded initialisation (the
sink is still being built, or the metrics loop exited before it got
there) fell into the same branch as a built client, so status could show
alerts=sns for a sink that did not exist. alertDeliveryProbe now sets SNS
only for the initialised state and SNSUnavailable only for a failed build;
the unknown state is neither. Tested for unknown, ok and failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev fb8f91925146e977f08b6cab001ce7caf9363760 parent 98a8a9e
2 files changed, +16 −3
  • internal/host/alertsink.go +7 −3
  • internal/host/alertsink_test.go +9 −0

CI passed — run details

diff --git a/internal/host/alertsink.go b/internal/host/alertsink.go
index 3fe144e02..c9aca34d8 100644
--- a/internal/host/alertsink.go
+++ b/internal/host/alertsink.go
@@ -173,10 +173,14 @@ func alertDeliveryProbe(getenv func(string) string, counts *alertCounters) *stat
SNSFailures: counts.sns.Load(),
}
if _, err := parseAlertTopicARN(getenv("CARLOS_ALERT_SNS_TOPIC_ARN")); err == nil {
- if counts.snsInit.Load() == snsInitFailed {
- out.SNSUnavailable = true
- } else {
+ // Only an initialised client is a sink. snsInitUnknown (not built
+ // yet, or the metrics loop never got there) is neither available
+ // nor unavailable: the box has no working SNS sink to report.
+ switch counts.snsInit.Load() {
+ case snsInitOK:
out.SNS = true
+ case snsInitFailed:
+ out.SNSUnavailable = true
}
}
return out
diff --git a/internal/host/alertsink_test.go b/internal/host/alertsink_test.go
index 412bd840d..bd162e674 100644
--- a/internal/host/alertsink_test.go
+++ b/internal/host/alertsink_test.go
@@ -317,6 +317,15 @@ func TestAlertDeliveryProbe(t *testing.T) {
t.Errorf("webhook only: %+v", got)
}
+ // A valid ARN alone is not a sink until buildAlertSinks has built the
+ // client: before that (a stalled build, or a metrics loop that exited
+ // first) the box must not claim sns.
+ got = alertDeliveryProbe(env(map[string]string{"CARLOS_ALERT_SNS_TOPIC_ARN": testTopicARN}), counts)
+ if got.SNS || got.SNSUnavailable || got.Delivered() {
+ t.Errorf("sink not yet built must not read as sns: %+v", got)
+ }
+
+ counts.snsInit.Store(snsInitOK)
got = alertDeliveryProbe(env(map[string]string{"CARLOS_ALERT_SNS_TOPIC_ARN": testTopicARN}), counts)
if got.Webhook || !got.SNS || !got.Delivered() {
t.Errorf("sns only: %+v", got)