prova-core / brain /tests /test_proactive.py
chintakapp's picture
core 2.0
210ef30
Raw History Blame Contribute Delete
5.63 kB
"""Proactive gates — port of ProactiveWorker.kt, blueprint 10.3.
2026-09-21 is a Monday and 2026-09-19 a Saturday. Times are IST.
"""
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import pytest
from brain.services.proactive import decide, in_quiet_hours
from brain.store.memory import MemoryStore
TZ = ZoneInfo("Asia/Kolkata")
USER = "u1"
def at(day: int, hour: int, minute: int = 0) -> datetime:
return datetime(2026, 9, day, hour, minute, tzinfo=TZ)
async def store_with_last(
now: datetime, minutes_ago: int, role: str = "user", proactive: bool = False
):
store = MemoryStore()
store.messages.append(
{
"id": "m",
"user_id": USER,
"mode": "gf",
"role": role,
"content": "x",
"is_proactive": proactive,
"created_at": now - timedelta(minutes=minutes_ago),
}
)
return store
@pytest.mark.parametrize(
("minute", "start", "end", "quiet"),
[
(60, 0, 420, True),
(420, 0, 420, False), # end is exclusive
(1400, 1380, 420, True), # wraps midnight: 23:00-07:00
(200, 1380, 420, True),
(720, 1380, 420, False),
(500, 600, 600, False), # an empty window is never quiet
],
)
def test_quiet_hours_including_windows_that_wrap_midnight(minute, start, end, quiet):
assert in_quiet_hours(minute, start, end) is quiet
async def test_everything_off_means_notifications_off():
now = at(21, 11)
store = await store_with_last(now, 600)
settings = {"proactive": {"gfEnabled": False, "normalEnabled": False}}
assert (await decide(store, USER, settings, now)).gate == "notifications_off"
async def test_weekday_quiet_hours_are_midnight_to_seven():
now = at(21, 6, 59)
store = await store_with_last(now, 600)
assert (await decide(store, USER, {}, now)).gate == "quiet_hours"
async def test_weekend_quiet_hours_start_later():
# 00:30 on a Saturday is still awake time at the weekend; weekday rules would say quiet.
now = at(19, 0, 30)
store = await store_with_last(now, 600)
decision = await decide(store, USER, {}, now)
assert decision.gate != "quiet_hours"
async def test_the_daily_cap():
now = at(21, 11)
store = await store_with_last(now, 600)
for _ in range(3):
await store.log_proactive(USER, "checkin", None, ok=True)
for row in store.proactive:
row["sent_at"] = now - timedelta(hours=1)
assert (await decide(store, USER, {}, now)).gate == "daily_cap"
async def test_the_cap_is_clamped_to_one_through_five():
now = at(21, 11)
store = await store_with_last(now, 600)
for _ in range(5):
await store.log_proactive(USER, "checkin", None, ok=True)
for row in store.proactive:
row["sent_at"] = now - timedelta(hours=1)
decision = await decide(store, USER, {"proactive": {"maxPerDay": 50}}, now)
assert decision.gate == "daily_cap"
async def test_no_history_means_no_opening_line():
assert (await decide(MemoryStore(), USER, {}, at(21, 11))).gate == "no_history"
async def test_a_weekday_needs_five_hours_of_silence():
now = at(21, 11)
assert (await decide(await store_with_last(now, 299), USER, {}, now)).gate == "too_recent"
assert (await decide(await store_with_last(now, 301), USER, {}, now)).send is True
async def test_the_weekend_bar_is_an_hour():
now = at(19, 15)
assert (await decide(await store_with_last(now, 61), USER, {}, now)).send is True
async def test_a_routine_window_lowers_the_bar_to_45_minutes_and_is_stamped():
now = at(21, 14, 10) # ten minutes after lunch
decision = await decide(await store_with_last(now, 50), USER, {}, now)
assert decision.send is True
assert decision.stamp == "2026-09-21:lunch"
async def test_a_window_fires_once_per_day():
now = at(21, 14, 10)
store = await store_with_last(now, 50)
await store.log_proactive(USER, "checkin", "2026-09-21:lunch", ok=True)
for row in store.proactive:
row["sent_at"] = now - timedelta(days=1) # does not count toward today's cap
decision = await decide(store, USER, {}, now)
assert decision.gate == "too_recent" # back to the five-hour bar
async def test_off_today_closes_the_routine_windows():
now = at(21, 14, 10)
settings = {"offTodayDate": "2026-09-21"}
decision = await decide(await store_with_last(now, 50), USER, settings, now)
assert decision.gate == "too_recent"
async def test_an_unanswered_check_in_blocks_the_next_one():
now = at(21, 17)
store = await store_with_last(now, 330, role="assistant", proactive=True)
assert (await decide(store, USER, {}, now)).gate == "unanswered_backoff"
async def test_a_recent_failure_backs_off_for_six_hours():
now = at(21, 17)
store = await store_with_last(now, 600)
await store.log_proactive(USER, "failed", None, ok=False)
store.proactive[-1]["sent_at"] = now - timedelta(hours=2)
assert (await decide(store, USER, {}, now)).gate == "failure_backoff"
async def test_test_mode_ignores_every_gate():
decision = await decide(MemoryStore(), USER, {}, at(21, 3), test_mode=True)
assert decision.send is True
assert decision.gate == "test_mode"
async def test_gf_is_the_default_mode_and_normal_is_used_only_when_gf_is_off():
now = at(21, 11)
store = await store_with_last(now, 600)
assert (await decide(store, USER, {}, now)).mode.value == "gf"
only_normal = {"proactive": {"gfEnabled": False, "normalEnabled": True}}
assert (await decide(store, USER, only_normal, now)).mode.value == "normal"