"""Tests for focus_nudge utility — should_nudge() logic and nudge_unity_focus() gating.""" import time from unittest.mock import patch, AsyncMock import pytest from utils.focus_nudge import ( should_nudge, reset_nudge_backoff, nudge_unity_focus, _is_available, ) class TestShouldNudge: """Tests for should_nudge() decision logic.""" def test_returns_false_when_not_running(self): assert should_nudge(status="succeeded", editor_is_focused=False, last_update_unix_ms=0, current_time_ms=99999) is False def test_returns_false_when_focused(self): assert should_nudge(status="running", editor_is_focused=True, last_update_unix_ms=0, current_time_ms=99999) is False def test_returns_true_when_stalled_and_unfocused(self): now_ms = int(time.time() * 1000) stale_ms = now_ms - 5000 # 5s ago assert should_nudge(status="running", editor_is_focused=False, last_update_unix_ms=stale_ms, current_time_ms=now_ms) is True def test_returns_false_when_recently_updated(self): now_ms = int(time.time() * 1000) recent_ms = now_ms - 1000 # 1s ago (within 3s threshold) assert should_nudge(status="running", editor_is_focused=False, last_update_unix_ms=recent_ms, current_time_ms=now_ms) is False def test_returns_true_when_no_updates_yet(self): """No last_update_unix_ms means tests might be stuck at start.""" assert should_nudge(status="running", editor_is_focused=False, last_update_unix_ms=None) is True def test_custom_stall_threshold(self): now_ms = int(time.time() * 1000) stale_ms = now_ms - 2000 # 2s ago # Default threshold (3s) — not stale yet assert should_nudge(status="running", editor_is_focused=False, last_update_unix_ms=stale_ms, current_time_ms=now_ms) is False # Custom threshold (1s) — stale assert should_nudge(status="running", editor_is_focused=False, last_update_unix_ms=stale_ms, current_time_ms=now_ms, stall_threshold_ms=1000) is True def test_returns_false_for_failed_status(self): assert should_nudge(status="failed", editor_is_focused=False, last_update_unix_ms=0, current_time_ms=99999) is False def test_returns_false_for_cancelled_status(self): assert should_nudge(status="cancelled", editor_is_focused=False, last_update_unix_ms=0, current_time_ms=99999) is False class TestResetNudgeBackoff: """Tests for reset_nudge_backoff() state management.""" def test_resets_consecutive_nudges(self): import utils.focus_nudge as fn fn._consecutive_nudges = 5 reset_nudge_backoff() assert fn._consecutive_nudges == 0 def test_updates_last_progress_time(self): import utils.focus_nudge as fn old_time = fn._last_progress_time reset_nudge_backoff() assert fn._last_progress_time >= old_time class TestNudgeUnityFocus: """Tests for nudge_unity_focus() gating logic.""" @pytest.mark.asyncio async def test_skips_when_not_available(self): with patch("utils.focus_nudge._is_available", return_value=False): result = await nudge_unity_focus(force=True) assert result is False @pytest.mark.asyncio async def test_skips_when_unity_already_focused(self): from utils.focus_nudge import _FrontmostAppInfo with patch("utils.focus_nudge._is_available", return_value=True), \ patch("utils.focus_nudge._get_frontmost_app", return_value=_FrontmostAppInfo(name="Unity")): result = await nudge_unity_focus(force=True) assert result is False @pytest.mark.asyncio async def test_skips_when_frontmost_app_unknown(self): with patch("utils.focus_nudge._is_available", return_value=True), \ patch("utils.focus_nudge._get_frontmost_app", return_value=None): result = await nudge_unity_focus(force=True) assert result is False @pytest.mark.asyncio async def test_rate_limited_by_backoff(self): import utils.focus_nudge as fn from utils.focus_nudge import _FrontmostAppInfo # Simulate a very recent nudge fn._last_nudge_time = time.monotonic() fn._consecutive_nudges = 0 with patch("utils.focus_nudge._is_available", return_value=True), \ patch("utils.focus_nudge._get_frontmost_app", return_value=_FrontmostAppInfo(name="Terminal")): result = await nudge_unity_focus(force=False) assert result is False