forked from Rockachopa/Timmy-time-dashboard
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
147 lines
4.0 KiB
Python
147 lines
4.0 KiB
Python
"""Tests for the time adapter — circadian awareness."""
|
|
|
|
from datetime import UTC, datetime
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from timmy.adapters.time_adapter import TimeAdapter, classify_period
|
|
|
|
# ---------- classify_period ----------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"hour, expected",
|
|
[
|
|
(6, "morning"),
|
|
(7, "morning"),
|
|
(8, "morning"),
|
|
(9, None),
|
|
(12, "afternoon"),
|
|
(13, "afternoon"),
|
|
(14, None),
|
|
(18, "evening"),
|
|
(19, "evening"),
|
|
(20, None),
|
|
(23, "late_night"),
|
|
(0, "late_night"),
|
|
(2, "late_night"),
|
|
(3, None),
|
|
(10, None),
|
|
(16, None),
|
|
],
|
|
)
|
|
def test_classify_period(hour: int, expected: str | None) -> None:
|
|
assert classify_period(hour) == expected
|
|
|
|
|
|
# ---------- record_interaction / time_since ----------
|
|
|
|
|
|
def test_time_since_last_interaction_none() -> None:
|
|
adapter = TimeAdapter()
|
|
assert adapter.time_since_last_interaction() is None
|
|
|
|
|
|
def test_time_since_last_interaction() -> None:
|
|
adapter = TimeAdapter()
|
|
t0 = datetime(2026, 3, 18, 10, 0, 0, tzinfo=UTC)
|
|
t1 = datetime(2026, 3, 18, 10, 5, 0, tzinfo=UTC)
|
|
adapter.record_interaction(now=t0)
|
|
assert adapter.time_since_last_interaction(now=t1) == 300.0
|
|
|
|
|
|
# ---------- tick — circadian events ----------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tick_emits_morning() -> None:
|
|
adapter = TimeAdapter()
|
|
now = datetime(2026, 3, 18, 7, 0, 0, tzinfo=UTC)
|
|
|
|
with patch("timmy.adapters.time_adapter.emit", new_callable=AsyncMock) as mock_emit:
|
|
emitted = await adapter.tick(now=now)
|
|
|
|
assert "time.morning" in emitted
|
|
mock_emit.assert_any_call(
|
|
"time.morning",
|
|
source="time_adapter",
|
|
data={"hour": 7, "period": "morning"},
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tick_emits_late_night() -> None:
|
|
adapter = TimeAdapter()
|
|
now = datetime(2026, 3, 19, 1, 0, 0, tzinfo=UTC)
|
|
|
|
with patch("timmy.adapters.time_adapter.emit", new_callable=AsyncMock) as mock_emit:
|
|
emitted = await adapter.tick(now=now)
|
|
|
|
assert "time.late_night" in emitted
|
|
mock_emit.assert_any_call(
|
|
"time.late_night",
|
|
source="time_adapter",
|
|
data={"hour": 1, "period": "late_night"},
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tick_no_duplicate_period() -> None:
|
|
"""Same period on consecutive ticks should not re-emit."""
|
|
adapter = TimeAdapter()
|
|
t1 = datetime(2026, 3, 18, 7, 0, 0, tzinfo=UTC)
|
|
t2 = datetime(2026, 3, 18, 7, 30, 0, tzinfo=UTC)
|
|
|
|
with patch("timmy.adapters.time_adapter.emit", new_callable=AsyncMock):
|
|
await adapter.tick(now=t1)
|
|
emitted = await adapter.tick(now=t2)
|
|
|
|
assert emitted == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tick_no_event_outside_periods() -> None:
|
|
adapter = TimeAdapter()
|
|
now = datetime(2026, 3, 18, 10, 0, 0, tzinfo=UTC)
|
|
|
|
with patch("timmy.adapters.time_adapter.emit", new_callable=AsyncMock) as mock_emit:
|
|
emitted = await adapter.tick(now=now)
|
|
|
|
assert emitted == []
|
|
mock_emit.assert_not_called()
|
|
|
|
|
|
# ---------- tick — new_day ----------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tick_emits_new_day() -> None:
|
|
adapter = TimeAdapter()
|
|
day1 = datetime(2026, 3, 18, 23, 30, 0, tzinfo=UTC)
|
|
day2 = datetime(2026, 3, 19, 0, 30, 0, tzinfo=UTC)
|
|
|
|
with patch("timmy.adapters.time_adapter.emit", new_callable=AsyncMock) as mock_emit:
|
|
await adapter.tick(now=day1)
|
|
emitted = await adapter.tick(now=day2)
|
|
|
|
assert "time.new_day" in emitted
|
|
mock_emit.assert_any_call(
|
|
"time.new_day",
|
|
source="time_adapter",
|
|
data={"date": "2026-03-19"},
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tick_no_new_day_same_date() -> None:
|
|
adapter = TimeAdapter()
|
|
t1 = datetime(2026, 3, 18, 10, 0, 0, tzinfo=UTC)
|
|
t2 = datetime(2026, 3, 18, 15, 0, 0, tzinfo=UTC)
|
|
|
|
with patch("timmy.adapters.time_adapter.emit", new_callable=AsyncMock):
|
|
await adapter.tick(now=t1)
|
|
emitted = await adapter.tick(now=t2)
|
|
|
|
assert "time.new_day" not in emitted
|