Skip to content

Commit 1d86033

Browse files
Sbussisoclaude
andcommitted
fix: use -inf sentinel in transition debounce so first emit fires on fresh VMs
The transition debounce used ``0.0`` as the "never-emitted" sentinel. On GitHub Actions runners where ``time.monotonic()`` starts near zero (it measures seconds since VM boot), ``now - 0.0`` can fall inside the 60s debounce window — silently suppressing the very first emit and breaking the four offline-sweep tests that rely on seeing a notification row. Tests passed on local dev boxes (hours of uptime → monotonic in the thousands) but failed on CI every time. Fix: sentinel becomes ``float("-inf")`` so the first emit always clears the threshold regardless of host uptime. Added a regression test that monkeypatches ``time.monotonic`` to 5.0 to lock the behaviour in. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cebeb11 commit 1d86033

2 files changed

Lines changed: 21 additions & 2 deletions

File tree

backend/app/api/notifications.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,16 @@ def create_notification(
174174
def _should_emit_transition(kind: str, entity_id: str, direction: str) -> bool:
175175
"""Return True if we haven't emitted this (kind,entity,direction)
176176
recently. Updates the timestamp on a positive return so subsequent
177-
calls within the debounce window are suppressed."""
177+
calls within the debounce window are suppressed.
178+
179+
The sentinel for "never emitted before" is ``-inf``, not ``0.0`` —
180+
otherwise on a freshly-booted host (e.g. GitHub Actions runners,
181+
where ``time.monotonic()`` starts near zero) the very first emit
182+
can fall inside the debounce window and get silently dropped.
183+
"""
178184
key = (kind, entity_id, direction)
179185
now = _time.monotonic()
180-
last = _transition_debounce.get(key, 0.0)
186+
last = _transition_debounce.get(key, float("-inf"))
181187
if now - last < _TRANSITION_DEBOUNCE_SECONDS:
182188
return False
183189
_transition_debounce[key] = now

backend/tests/test_notifications.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,19 @@ def test_transition_different_cameras_not_debounced(db):
298298
assert db.query(Notification).count() == 2
299299

300300

301+
def test_transition_emits_on_fresh_boot_low_monotonic(db, monkeypatch):
302+
# Regression: GitHub Actions runners can have a very small ``time.monotonic()``
303+
# value (freshly-booted VM), which would collide with the 60s debounce window
304+
# if the "never-emitted" sentinel was 0.0. Simulate that here and make sure
305+
# the first emit still fires.
306+
monkeypatch.setattr("app.api.notifications._time.monotonic", lambda: 5.0)
307+
emit_camera_transition(
308+
db, camera_id="cam_fresh_boot", org_id="org_test123",
309+
display_name="Fresh Boot", new_status="offline",
310+
)
311+
assert db.query(Notification).count() == 1
312+
313+
301314
# ── Offline sweep ─────────────────────────────────────────────────
302315

303316
def _make_node(db, *, node_id, org_id, status, last_seen_minutes_ago, name=None):

0 commit comments

Comments
 (0)