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
This commit is contained in:
2026-05-29 14:05:29 +02:00
parent d9632a3412
commit 3da2bc93cb
2 changed files with 39 additions and 6 deletions
+8
View File
@@ -118,6 +118,14 @@ async def publish_lifecycle_event(
await event_bus.publish(event_type, payload) await event_bus.publish(event_type, payload)
# Create notification for instance owner (fire-and-forget) # 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" severity = "error" if event_type == "instance.error" else "info"
title = _derive_title(event_type) title = _derive_title(event_type)
@@ -88,12 +88,12 @@ async def test_instance(db_session: AsyncSession) -> ToolInstance:
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.integration @pytest.mark.integration
async def test_lifecycle_event_creates_notification( async def test_lifecycle_started_intermediate_skips_notification(
db_session: AsyncSession, db_session: AsyncSession,
event_bus: InstanceEventBus, event_bus: InstanceEventBus,
test_instance: ToolInstance, test_instance: ToolInstance,
) -> None: ) -> None:
"""Triggering a lifecycle event creates a notification for the instance owner.""" """Intermediate 'starting' state does NOT create a notification."""
received: list[InstanceEventPayload] = [] received: list[InstanceEventPayload] = []
def subscriber(payload: InstanceEventPayload) -> None: def subscriber(payload: InstanceEventPayload) -> None:
@@ -109,13 +109,39 @@ async def test_lifecycle_event_creates_notification(
instance=test_instance, instance=test_instance,
event_type="instance.started", event_type="instance.started",
status="starting", status="starting",
message="Container started", message="Container starting...",
) )
# Event still published # Event still published
assert len(received) == 1 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( result = await db_session.execute(
select(Notification).where(Notification.user_id == test_instance.owner_id) select(Notification).where(Notification.user_id == test_instance.owner_id)
) )
@@ -124,10 +150,9 @@ async def test_lifecycle_event_creates_notification(
n = notifications[0] n = notifications[0]
assert n.category == "instance" assert n.category == "instance"
assert n.severity == "info" assert n.severity == "info"
assert n.title == "Container started" assert n.title == "Health Changed"
assert n.source_type == "tool_instances" assert n.source_type == "tool_instances"
assert n.source_id == test_instance.id assert n.source_id == test_instance.id
assert n.message == "Container started"
@pytest.mark.asyncio @pytest.mark.asyncio