/** * @file SchedulerModuleTests.cpp * @brief Integration tests for SchedulerModule (10 TI) */ #include #include "mocks/MockIO.hpp" #include "utils/TimeSimulator.hpp" #include "utils/TestHelpers.hpp" #include "modules/SchedulerModule.h" #include using namespace aissia; using namespace aissia::tests; // ============================================================================ // Test Fixture // ============================================================================ class SchedulerTestFixture { public: MockIO io; TimeSimulator time; SchedulerModule module; void configure(const json& config = json::object()) { json fullConfig = { {"hyperfocusThresholdMinutes", 120}, {"breakReminderIntervalMinutes", 45}, {"breakDurationMinutes", 10} }; fullConfig.merge_patch(config); grove::JsonDataNode configNode("config", fullConfig); module.setConfiguration(configNode, &io, nullptr); } void process() { grove::JsonDataNode input("input", time.createInput()); module.process(input); } void processWithTime(float gameTime) { time.setTime(gameTime); grove::JsonDataNode input("input", time.createInput(0.1f)); module.process(input); } }; // ============================================================================ // TI_SCHEDULER_001: Start Task // ============================================================================ TEST_CASE("TI_SCHEDULER_001_StartTask", "[scheduler][integration]") { SchedulerTestFixture f; f.configure(); // Inject task switch message f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}}); // Process f.process(); // Verify task_started was published REQUIRE(f.io.wasPublished("scheduler:task_started")); auto msg = f.io.getLastPublished("scheduler:task_started"); REQUIRE(msg["taskId"] == "task-1"); REQUIRE(msg.contains("taskName")); } // ============================================================================ // TI_SCHEDULER_002: Complete Task // ============================================================================ TEST_CASE("TI_SCHEDULER_002_CompleteTask", "[scheduler][integration]") { SchedulerTestFixture f; f.configure(); // Start a task at time 0 f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}}); f.processWithTime(0.0f); f.io.clearPublished(); // Advance time 30 minutes (1800 seconds) f.time.setTime(1800.0f); // Switch to another task (completes current task implicitly) f.io.injectMessage("user:task_switch", {{"taskId", "task-2"}}); f.process(); // Verify task_completed was published with duration REQUIRE(f.io.wasPublished("scheduler:task_completed")); auto msg = f.io.getLastPublished("scheduler:task_completed"); REQUIRE(msg["taskId"] == "task-1"); REQUIRE(msg.contains("duration")); // Duration should be around 30 minutes int duration = msg["duration"].get(); REQUIRE(duration >= 29); REQUIRE(duration <= 31); } // ============================================================================ // TI_SCHEDULER_003: Hyperfocus Detection // ============================================================================ TEST_CASE("TI_SCHEDULER_003_HyperfocusDetection", "[scheduler][integration]") { SchedulerTestFixture f; f.configure({{"hyperfocusThresholdMinutes", 120}}); // Start a task at time 0 f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}}); f.processWithTime(0.0f); f.io.clearPublished(); // Advance time past threshold (121 minutes = 7260 seconds) f.processWithTime(7260.0f); // Verify hyperfocus alert REQUIRE(f.io.wasPublished("scheduler:hyperfocus_alert")); auto msg = f.io.getLastPublished("scheduler:hyperfocus_alert"); REQUIRE(msg["type"] == "hyperfocus"); REQUIRE(msg["task"] == "task-1"); REQUIRE(msg["duration_minutes"].get() >= 120); } // ============================================================================ // TI_SCHEDULER_004: Hyperfocus Alert Only Once // ============================================================================ TEST_CASE("TI_SCHEDULER_004_HyperfocusAlertOnce", "[scheduler][integration]") { SchedulerTestFixture f; f.configure({{"hyperfocusThresholdMinutes", 120}}); // Start task f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}}); f.processWithTime(0.0f); // Trigger hyperfocus (121 min) f.processWithTime(7260.0f); // Count first alert size_t alertCount = f.io.countPublished("scheduler:hyperfocus_alert"); REQUIRE(alertCount == 1); // Continue processing (130 min, 140 min) f.processWithTime(7800.0f); f.processWithTime(8400.0f); // Should still be only 1 alert REQUIRE(f.io.countPublished("scheduler:hyperfocus_alert") == 1); } // ============================================================================ // TI_SCHEDULER_005: Break Reminder // ============================================================================ TEST_CASE("TI_SCHEDULER_005_BreakReminder", "[scheduler][integration]") { SchedulerTestFixture f; f.configure({{"breakReminderIntervalMinutes", 45}}); // Process at time 0 (sets lastBreakTime) f.processWithTime(0.0f); f.io.clearPublished(); // Advance past break reminder interval (46 minutes = 2760 seconds) f.processWithTime(2760.0f); // Verify break reminder REQUIRE(f.io.wasPublished("scheduler:break_reminder")); auto msg = f.io.getLastPublished("scheduler:break_reminder"); REQUIRE(msg["type"] == "break"); REQUIRE(msg.contains("break_duration")); } // ============================================================================ // TI_SCHEDULER_006: Idle Pauses Session // ============================================================================ TEST_CASE("TI_SCHEDULER_006_IdlePausesSession", "[scheduler][integration]") { SchedulerTestFixture f; f.configure(); // Start task f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}}); f.processWithTime(0.0f); // Go idle f.io.injectMessage("monitoring:idle_detected", {{"idleSeconds", 300}}); f.processWithTime(60.0f); // Verify module received and processed the idle message // (Module logs "User idle" - we can verify via state) auto state = f.module.getState(); REQUIRE(state != nullptr); // Task should still be tracked (idle doesn't clear it) REQUIRE(state->getString("currentTaskId", "") == "task-1"); } // ============================================================================ // TI_SCHEDULER_007: Activity Resumes Session // ============================================================================ TEST_CASE("TI_SCHEDULER_007_ActivityResumesSession", "[scheduler][integration]") { SchedulerTestFixture f; f.configure(); // Start task, go idle, resume f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}}); f.processWithTime(0.0f); f.io.injectMessage("monitoring:idle_detected", {}); f.processWithTime(60.0f); f.io.injectMessage("monitoring:activity_resumed", {}); f.processWithTime(120.0f); // Verify session continues - task still active auto state = f.module.getState(); REQUIRE(state != nullptr); REQUIRE(state->getString("currentTaskId", "") == "task-1"); } // ============================================================================ // TI_SCHEDULER_008: Tool Query Get Current Task // ============================================================================ TEST_CASE("TI_SCHEDULER_008_ToolQueryGetCurrentTask", "[scheduler][integration]") { SchedulerTestFixture f; f.configure(); // Start a task f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}}); f.processWithTime(0.0f); f.io.clearPublished(); // Query current task f.io.injectMessage("scheduler:query", { {"action", "get_current_task"}, {"correlation_id", "test-123"} }); f.processWithTime(60.0f); // Verify response REQUIRE(f.io.wasPublished("scheduler:response")); auto resp = f.io.getLastPublished("scheduler:response"); REQUIRE(resp["correlation_id"] == "test-123"); REQUIRE(resp["task_id"] == "task-1"); } // ============================================================================ // TI_SCHEDULER_009: Tool Command Start Break // ============================================================================ TEST_CASE("TI_SCHEDULER_009_ToolCommandStartBreak", "[scheduler][integration]") { SchedulerTestFixture f; f.configure(); // Start task f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}}); f.processWithTime(0.0f); f.io.clearPublished(); // Command to start break f.io.injectMessage("scheduler:command", { {"action", "start_break"}, {"duration_minutes", 15}, {"reason", "test break"} }); f.processWithTime(60.0f); // Verify break started was published REQUIRE(f.io.wasPublished("scheduler:break_started")); auto msg = f.io.getLastPublished("scheduler:break_started"); REQUIRE(msg["duration"] == 15); REQUIRE(msg["reason"] == "test break"); // Verify response was also published REQUIRE(f.io.wasPublished("scheduler:response")); auto resp = f.io.getLastPublished("scheduler:response"); REQUIRE(resp["success"] == true); } // ============================================================================ // TI_SCHEDULER_010: State Serialization // ============================================================================ TEST_CASE("TI_SCHEDULER_010_StateSerialization", "[scheduler][integration]") { SchedulerTestFixture f; f.configure(); // Setup some state f.io.injectMessage("user:task_switch", {{"taskId", "task-1"}}); f.processWithTime(0.0f); f.processWithTime(1800.0f); // 30 minutes // Get state auto state = f.module.getState(); REQUIRE(state != nullptr); // Verify state content REQUIRE(state->getString("currentTaskId", "") == "task-1"); REQUIRE(state->getBool("hyperfocusAlertSent", true) == false); // Create new module and restore state SchedulerModule module2; MockIO io2; grove::JsonDataNode configNode("config", json::object()); module2.setConfiguration(configNode, &io2, nullptr); module2.setState(*state); // Verify state was restored auto state2 = module2.getState(); REQUIRE(state2 != nullptr); REQUIRE(state2->getString("currentTaskId", "") == "task-1"); REQUIRE(state2->getBool("hyperfocusAlertSent", true) == false); }