96 lines
2.8 KiB
Python
96 lines
2.8 KiB
Python
"""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()
|