1
0

test_forum_recovery.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. from datetime import datetime, timezone
  2. from unittest.mock import AsyncMock, MagicMock, patch
  3. import pytest
  4. from app.services.forum_scheduler import ForumScheduler, forum_deadline_epoch, to_epoch_seconds
  5. @pytest.mark.parametrize(
  6. ("persisted_value", "expected"),
  7. [
  8. (datetime(2026, 8, 12, 12, 0, 0, tzinfo=timezone.utc), 1786536000.0),
  9. ("2026-08-12T12:00:00+00:00", 1786536000.0),
  10. (1786536000, 1786536000.0),
  11. (1786536000000, 1786536000.0),
  12. ],
  13. )
  14. def test_recovery_clock_normalizes_datetime_representations(persisted_value, expected):
  15. assert to_epoch_seconds(persisted_value) == expected
  16. def test_thirty_minute_deadline_is_exactly_1800_seconds():
  17. start = datetime(2026, 8, 13, 8, 0, 0, tzinfo=timezone.utc)
  18. assert forum_deadline_epoch(start, 30) - to_epoch_seconds(start) == 1800
  19. @pytest.mark.asyncio
  20. async def test_recover_running_forums_restarts_persisted_tasks():
  21. scheduler = ForumScheduler()
  22. result = MagicMock()
  23. result.rows = [(3,), (7,)]
  24. result.columns = ["id"]
  25. db = MagicMock()
  26. db.execute.return_value = result
  27. with patch.object(scheduler, "_get_db") as get_db, patch.object(
  28. scheduler, "start_forum", new_callable=AsyncMock
  29. ) as start_forum:
  30. get_db.return_value.__enter__.return_value = db
  31. recovered = await scheduler.recover_running_forums()
  32. assert recovered == [3, 7]
  33. assert [call.args[0] for call in start_forum.await_args_list] == [3, 7]
  34. assert all(call.kwargs == {"recovering": True} for call in start_forum.await_args_list)
  35. @pytest.mark.asyncio
  36. async def test_recovered_forum_skips_opening_and_uses_persisted_clock():
  37. scheduler = ForumScheduler()
  38. forum = MagicMock(
  39. id=3,
  40. status="running",
  41. duration_minutes=30,
  42. start_time="2026-08-12 12:00:00",
  43. moderator=None,
  44. summary_history=[],
  45. ablation_flags='{"mock_llm": true}',
  46. )
  47. with patch.object(scheduler, "_get_db") as get_db, patch(
  48. "app.services.forum_scheduler.get_forum",
  49. side_effect=[forum, forum],
  50. ), patch(
  51. "app.services.forum_scheduler.get_forum_participants", return_value=[]
  52. ), patch(
  53. "app.services.forum_scheduler.get_forum_messages", return_value=[]
  54. ), patch(
  55. "app.services.forum_scheduler.update_forum"
  56. ) as update_forum, patch.object(
  57. scheduler, "_broadcast_system_message", new_callable=AsyncMock
  58. ) as broadcast_system_message, patch.object(
  59. scheduler, "_broadcast_system_log", new_callable=AsyncMock
  60. ), patch.object(
  61. scheduler, "_moderator_speak", new_callable=AsyncMock
  62. ) as moderator_speak, patch.object(
  63. scheduler, "_flush_logs_to_db", new_callable=AsyncMock
  64. ), patch(
  65. "app.services.forum_scheduler.restore_framework_history"
  66. ), patch(
  67. "app.services.forum_scheduler.ModeratorAgent"
  68. ), patch(
  69. "app.services.forum_scheduler.time.time",
  70. return_value=datetime.fromisoformat("2026-08-12 12:30:00").timestamp(),
  71. ), patch(
  72. "app.services.forum_scheduler.manager.broadcast", new_callable=AsyncMock
  73. ):
  74. get_db.return_value.__enter__.return_value = MagicMock()
  75. await scheduler._run_forum_loop(3, recovering=True)
  76. broadcast_system_message.assert_not_awaited()
  77. assert not any(call.args[2] == "opening" for call in moderator_speak.await_args_list)
  78. assert any(call.args[2] == "closing" for call in moderator_speak.await_args_list)
  79. assert not any(call.kwargs.get("start_time") for call in update_forum.call_args_list)
  80. assert any(call.kwargs.get("ablation_flags", {}).get("mock_llm") for call in moderator_speak.call_args_list)
  81. def test_start_persists_controlled_flags_before_scheduling():
  82. from app.services.forum_service import ForumService
  83. db = MagicMock()
  84. forum = MagicMock(creator_id=1, status="pending", start_time=None, duration_minutes=30)
  85. service = ForumService(db)
  86. with patch("app.services.forum_service.get_forum", return_value=forum), patch(
  87. "app.services.forum_service.update_forum"
  88. ) as update_forum, patch(
  89. "app.services.forum_service.scheduler.start_forum", new_callable=AsyncMock
  90. ) as start_forum:
  91. result = __import__("asyncio").run(
  92. service.start_forum(9, user_id=1, ablation_flags={"mock_llm": True})
  93. )
  94. assert result["status"] == "started"
  95. update_forum.assert_called_once()
  96. assert update_forum.call_args.args == (db, 9)
  97. assert update_forum.call_args.kwargs["status"] == "running"
  98. assert update_forum.call_args.kwargs["ablation_flags"] == {"mock_llm": True}
  99. assert isinstance(update_forum.call_args.kwargs["start_time"], datetime)
  100. assert update_forum.call_args.kwargs["start_time"].tzinfo is timezone.utc
  101. assert result["duration_minutes"] == 30
  102. assert result["start_time"] == update_forum.call_args.kwargs["start_time"]
  103. start_forum.assert_awaited_once_with(9, {"mock_llm": True})
  104. @pytest.mark.asyncio
  105. async def test_shutdown_preserves_database_state():
  106. scheduler = ForumScheduler()
  107. async def wait_forever():
  108. await __import__("asyncio").Event().wait()
  109. task = __import__("asyncio").create_task(wait_forever())
  110. scheduler.running_tasks[1] = task
  111. with patch("app.services.forum_scheduler.update_forum") as update_forum:
  112. await scheduler.shutdown()
  113. assert task.cancelled()
  114. update_forum.assert_not_called()