From 3da2bc93cb055184bbb8c65437459fab09716000 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Fri, 29 May 2026 14:05:29 +0200 Subject: [PATCH] fix: skip intermediate 'starting' notifications, only notify on failed/successful attempts - lifecycle_hooks.publish_lifecycle_event now skips notification creation when event_type='instance.started' and status='starting' - Users only see notifications for terminal states: - Failed: instance.error - Successful: instance.health_changed with status='running' - Updated integration tests to verify new behavior: - test_lifecycle_started_intermediate_skips_notification - test_lifecycle_running_creates_notification Quality gates: pytest 42 passed, ruff clean --- apps/api/src/services/lifecycle_hooks.py | 8 ++++ .../test_notifications_lifecycle.py | 37 ++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/apps/api/src/services/lifecycle_hooks.py b/apps/api/src/services/lifecycle_hooks.py index 553fec0..17c07e8 100644 --- a/apps/api/src/services/lifecycle_hooks.py +++ b/apps/api/src/services/lifecycle_hooks.py @@ -118,6 +118,14 @@ async def publish_lifecycle_event( await event_bus.publish(event_type, payload) # Create notification for instance owner (fire-and-forget) + # Skip intermediate "starting" notifications — only notify on terminal states + # (failed or successful attempts) + _is_starting_intermediate = event_type == "instance.started" and ( + status or instance.status + ) == "starting" + if _is_starting_intermediate: + return + severity = "error" if event_type == "instance.error" else "info" title = _derive_title(event_type) diff --git a/apps/api/tests/integration/test_notifications_lifecycle.py b/apps/api/tests/integration/test_notifications_lifecycle.py index e6e9cfa..3e76fac 100644 --- a/apps/api/tests/integration/test_notifications_lifecycle.py +++ b/apps/api/tests/integration/test_notifications_lifecycle.py @@ -88,12 +88,12 @@ async def test_instance(db_session: AsyncSession) -> ToolInstance: @pytest.mark.asyncio @pytest.mark.integration -async def test_lifecycle_event_creates_notification( +async def test_lifecycle_started_intermediate_skips_notification( db_session: AsyncSession, event_bus: InstanceEventBus, test_instance: ToolInstance, ) -> None: - """Triggering a lifecycle event creates a notification for the instance owner.""" + """Intermediate 'starting' state does NOT create a notification.""" received: list[InstanceEventPayload] = [] def subscriber(payload: InstanceEventPayload) -> None: @@ -109,13 +109,39 @@ async def test_lifecycle_event_creates_notification( instance=test_instance, event_type="instance.started", status="starting", - message="Container started", + message="Container starting...", ) # Event still published assert len(received) == 1 - # Notification created + # No notification created for intermediate state + result = await db_session.execute( + select(Notification).where(Notification.user_id == test_instance.owner_id) + ) + notifications = list(result.scalars().all()) + assert len(notifications) == 0 + + +@pytest.mark.asyncio +@pytest.mark.integration +async def test_lifecycle_running_creates_notification( + db_session: AsyncSession, + event_bus: InstanceEventBus, + test_instance: ToolInstance, +) -> None: + """Successful terminal state (running) creates a notification.""" + from src.services.lifecycle_hooks import publish_lifecycle_event + + await publish_lifecycle_event( + event_bus=event_bus, + session=db_session, + instance=test_instance, + event_type="instance.health_changed", + status="running", + message="Container running", + ) + result = await db_session.execute( select(Notification).where(Notification.user_id == test_instance.owner_id) ) @@ -124,10 +150,9 @@ async def test_lifecycle_event_creates_notification( n = notifications[0] assert n.category == "instance" assert n.severity == "info" - assert n.title == "Container started" + assert n.title == "Health Changed" assert n.source_type == "tool_instances" assert n.source_id == test_instance.id - assert n.message == "Container started" @pytest.mark.asyncio