316 lines
11 KiB
C++
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);
|
|
}
|