"""Tests for the inference framework and time-of-day #112 proof.""" from __future__ import annotations import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..")) import pytest from datetime import datetime, timezone from ml.agents.inference.history import FeedbackEvent, UserHistory from ml.agents.inference.framework import run_inference from ml.agents.time_of_day import TimeOfDayAgent, MANIFEST as TOD_MANIFEST, MANIFEST from ml.agents.base import AgentInput _NOW = datetime(2026, 5, 1, 14, 0, 0, tzinfo=timezone.utc) # Thursday 14:00 def _inp(**kwargs) -> AgentInput: defaults = dict(user_id="u1", tasks=[], profile={}, now=_NOW, agent_prefs={}) defaults.update(kwargs) return AgentInput(**defaults) def _event(action: str, hour: int) -> FeedbackEvent: ts = f"2026-05-01T{hour:02d}:00:00+00:00" return FeedbackEvent(action=action, dwell_ms=60_000 if action == "done" else 500, created_at=ts) class TestRunInference: def test_cold_start_when_below_min_history(self): history = UserHistory(user_id="u1", events=[_event("done", 9)] * 5) # only 5 < 10 result = run_inference(TOD_MANIFEST, history) assert result["preferred_hour"] is None # cold_start_default def test_infers_preferred_hour_as_mode(self): # 7 events at 09:00, 3 at 17:00 → preferred_hour should be 9 events = [_event("done", 9)] * 7 + [_event("done", 17)] * 3 history = UserHistory(user_id="u1", events=events) result = run_inference(TOD_MANIFEST, history) assert result["preferred_hour"] == 9 def test_infers_preferred_hour_from_majority_hour(self): events = [_event("done", 20)] * 6 + [_event("done", 8)] * 4 history = UserHistory(user_id="u1", events=events) result = run_inference(TOD_MANIFEST, history) assert result["preferred_hour"] == 20 def test_no_inferred_params_returns_empty(self): from ml.agents.manifest import AgentManifest bare = AgentManifest( id="bare", version="1.0.0", description="", pref_schema={}, context_schema=[], required_consents=[], output_contract={}, ttl_sec=300, ) history = UserHistory(user_id="u1", events=[_event("done", 9)] * 20) result = run_inference(bare, history) assert result == {} def test_cold_start_fallback_on_infer_error(self): """infer() raising should fall back to cold_start_default, not crash.""" from ml.agents.manifest import InferredParam, AgentManifest def _bad_infer(h): raise RuntimeError("oops") m = AgentManifest( id="boom", version="1.0.0", description="", pref_schema={}, context_schema=[], required_consents=[], output_contract={}, ttl_sec=300, inferred_params=[InferredParam(key="x", ttl_sec=60, cold_start_default=42, min_history=1, infer=_bad_infer)], ) history = UserHistory(user_id="u1", events=[_event("done", 9)] * 5) result = run_inference(m, history) assert result["x"] == 42 class TestTimeOfDayAgentWithInference: agent = TimeOfDayAgent() def test_uses_preferred_hour_from_agent_prefs(self): inp = _inp(agent_prefs={"preferred_hour": 9}, now=datetime(2026, 5, 1, 9, 0, 0, tzinfo=timezone.utc)) out = self.agent.compute(inp) assert "peak productivity hour" in out.prompt_text.lower() or "peak" in out.prompt_text def test_quiet_window_noon_suppressed(self): inp = _inp( agent_prefs={"quiet_start": "22:00", "quiet_end": "07:00"}, now=datetime(2026, 5, 1, 23, 0, 0, tzinfo=timezone.utc), ) out = self.agent.compute(inp) assert "quiet window" in out.prompt_text def test_quiet_window_not_in_window(self): inp = _inp( agent_prefs={"quiet_start": "22:00", "quiet_end": "07:00"}, now=datetime(2026, 5, 1, 14, 0, 0, tzinfo=timezone.utc), ) out = self.agent.compute(inp) assert "quiet window" not in out.prompt_text def test_agent_prefs_override_profile(self): # agent_prefs.preferred_hour wins over profile.preferred_hour inp = _inp( profile={"preferred_hour": 8}, agent_prefs={"preferred_hour": 14}, now=datetime(2026, 5, 1, 14, 0, 0, tzinfo=timezone.utc), ) out = self.agent.compute(inp) assert "peak productivity hour (14:00)" in out.prompt_text def test_no_prefs_falls_back_to_profile(self): inp = _inp(profile={"preferred_hour": 10}, now=datetime(2026, 5, 1, 10, 0, 0, tzinfo=timezone.utc)) out = self.agent.compute(inp) assert "peak" in out.prompt_text def test_version_bumped(self): assert MANIFEST.version == "1.2.0" def test_manifest_has_preferred_hour_param(self): keys = {p.key for p in MANIFEST.inferred_params} assert "preferred_hour" in keys