forked from Rockachopa/Timmy-time-dashboard
fix: watch presence.json and broadcast state via WS (#379)
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
This commit is contained in:
95
tests/integrations/test_presence_watcher.py
Normal file
95
tests/integrations/test_presence_watcher.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Tests for the presence file watcher in dashboard.app."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Common patches to eliminate delays and inject mock ws_manager
|
||||
_FAST = {
|
||||
"dashboard.app._PRESENCE_POLL_SECONDS": 0.01,
|
||||
"dashboard.app._PRESENCE_INITIAL_DELAY": 0,
|
||||
}
|
||||
|
||||
|
||||
def _patches(mock_ws, presence_file):
|
||||
"""Return a combined context manager for presence watcher patches."""
|
||||
from contextlib import ExitStack
|
||||
|
||||
stack = ExitStack()
|
||||
stack.enter_context(patch("dashboard.app._PRESENCE_FILE", presence_file))
|
||||
stack.enter_context(patch("infrastructure.ws_manager.handler.ws_manager", mock_ws))
|
||||
for key, val in _FAST.items():
|
||||
stack.enter_context(patch(key, val))
|
||||
return stack
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_presence_watcher_broadcasts_on_file_change(tmp_path):
|
||||
"""Watcher reads presence.json and broadcasts via ws_manager."""
|
||||
from dashboard.app import _presence_watcher
|
||||
|
||||
presence_file = tmp_path / "presence.json"
|
||||
state = {
|
||||
"version": 1,
|
||||
"liveness": "2026-03-18T21:47:12Z",
|
||||
"current_focus": "Reviewing PR #267",
|
||||
"mood": "focused",
|
||||
}
|
||||
presence_file.write_text(json.dumps(state))
|
||||
|
||||
mock_ws = AsyncMock()
|
||||
|
||||
with _patches(mock_ws, presence_file):
|
||||
task = asyncio.create_task(_presence_watcher())
|
||||
await asyncio.sleep(0.15)
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
mock_ws.broadcast.assert_called_with("timmy_state", state)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_presence_watcher_synthesised_state_when_missing(tmp_path):
|
||||
"""Watcher broadcasts synthesised idle state when file is absent."""
|
||||
from dashboard.app import _SYNTHESIZED_STATE, _presence_watcher
|
||||
|
||||
missing_file = tmp_path / "no-such-file.json"
|
||||
mock_ws = AsyncMock()
|
||||
|
||||
with _patches(mock_ws, missing_file):
|
||||
task = asyncio.create_task(_presence_watcher())
|
||||
await asyncio.sleep(0.15)
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
mock_ws.broadcast.assert_called_with("timmy_state", _SYNTHESIZED_STATE)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_presence_watcher_handles_bad_json(tmp_path):
|
||||
"""Watcher logs warning on malformed JSON and doesn't crash."""
|
||||
from dashboard.app import _presence_watcher
|
||||
|
||||
presence_file = tmp_path / "presence.json"
|
||||
presence_file.write_text("{bad json!!!")
|
||||
mock_ws = AsyncMock()
|
||||
|
||||
with _patches(mock_ws, presence_file):
|
||||
task = asyncio.create_task(_presence_watcher())
|
||||
await asyncio.sleep(0.15)
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
# Should not have broadcast anything on bad JSON
|
||||
mock_ws.broadcast.assert_not_called()
|
||||
Reference in New Issue
Block a user