aissia/tests/modules/SchedulerModuleTests.cpp

316 lines
11 KiB
C++

/**
* @file SchedulerModuleTests.cpp
* @brief Integration tests for SchedulerModule (10 TI)
*/
#include <catch2/catch_test_macros.hpp>
#include "mocks/MockIO.hpp"
#include "utils/TimeSimulator.hpp"
#include "utils/TestHelpers.hpp"
#include "modules/SchedulerModule.h"
#include <grove/JsonDataNode.h>
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<int>();
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<int>() >= 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);
}