fix: Fix 8 failing module tests (now 110/110 passing)

Module fixes:
- MonitoringModule: Publish app_changed for first app (empty oldApp)
- MonitoringModule: Add appName and classification fields to events
- AIModule: Publish ai:suggestion in handleLLMResponse
- VoiceModule: Support flat config format (ttsEnabled/sttEnabled)
- StorageModule: Support both durationMinutes and duration fields

Test fixes:
- AIModuleTests: Simulate LLM responses for hyperfocus/break tests

All 110 tests now pass (252 assertions)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-11-28 13:44:41 +08:00
parent 58d7ca4355
commit d56993a48e
5 changed files with 63 additions and 16 deletions

View File

@ -114,6 +114,9 @@ void AIModule::handleLLMResponse(const grove::IDataNode& data) {
event->setString("text", text);
event->setInt("tokens", tokens);
m_io->publish("ai:response", std::move(event));
// Also publish as suggestion (for alerts and reminders)
publishSuggestion(text, 0);
}
}

View File

@ -137,13 +137,16 @@ void MonitoringModule::handleWindowChanged(const grove::IDataNode& data) {
std::string newApp = data.getString("newApp", "");
int duration = data.getInt("duration", 0);
bool wasProductive = false;
bool wasDistracting = false;
if (!oldApp.empty() && duration > 0) {
// Update duration tracking
m_appDurations[oldApp] += duration;
// Update productivity counters
bool wasProductive = isProductiveApp(oldApp);
bool wasDistracting = isDistractingApp(oldApp);
wasProductive = isProductiveApp(oldApp);
wasDistracting = isDistractingApp(oldApp);
if (wasProductive) {
m_totalProductiveSeconds += duration;
@ -151,21 +154,30 @@ void MonitoringModule::handleWindowChanged(const grove::IDataNode& data) {
m_totalDistractingSeconds += duration;
}
// Publish enriched app change event
if (m_io) {
auto event = std::make_unique<grove::JsonDataNode>("changed");
event->setString("oldApp", oldApp);
event->setString("newApp", newApp);
event->setInt("duration", duration);
event->setBool("wasProductive", wasProductive);
event->setBool("wasDistracting", wasDistracting);
m_io->publish("monitoring:app_changed", std::move(event));
}
m_logger->debug("App: {} -> {} ({}s, prod={})",
oldApp, newApp, duration, wasProductive);
}
// Always publish app change event (even for first app or zero duration)
if (m_io && !newApp.empty()) {
// Classify the new app
bool isProductive = isProductiveApp(newApp);
bool isDistracting = isDistractingApp(newApp);
std::string classification = "neutral";
if (isProductive) classification = "productive";
else if (isDistracting) classification = "distracting";
auto event = std::make_unique<grove::JsonDataNode>("changed");
event->setString("appName", newApp); // Primary field for current app
event->setString("oldApp", oldApp);
event->setString("newApp", newApp);
event->setInt("duration", duration);
event->setBool("wasProductive", wasProductive);
event->setBool("wasDistracting", wasDistracting);
event->setString("classification", classification);
m_io->publish("monitoring:app_changed", std::move(event));
}
m_currentApp = newApp;
}

View File

@ -162,7 +162,8 @@ void StorageModule::handleToolCommand(const grove::IDataNode& request) {
void StorageModule::handleTaskCompleted(const grove::IDataNode& data) {
std::string taskName = data.getString("taskName", "unknown");
int duration = data.getInt("duration", 0);
// Support both "durationMinutes" and "duration" fields
int duration = data.getInt("durationMinutes", data.getInt("duration", 0));
bool hyperfocus = data.getBool("hyperfocus", false);
m_logger->debug("Task completed: {} ({}min), publishing save request", taskName, duration);

View File

@ -18,17 +18,24 @@ void VoiceModule::setConfiguration(const grove::IDataNode& configNode,
m_io = io;
m_config = std::make_unique<grove::JsonDataNode>("config");
// TTS config
// TTS config - support both nested and flat formats
auto* ttsNode = const_cast<grove::IDataNode&>(configNode).getChildReadOnly("tts");
if (ttsNode) {
m_ttsEnabled = ttsNode->getBool("enabled", true);
} else {
// Fallback to flat format for tests
m_ttsEnabled = configNode.getBool("ttsEnabled", true);
}
// STT config
// STT config - support both nested and flat formats
auto* sttNode = const_cast<grove::IDataNode&>(configNode).getChildReadOnly("stt");
if (sttNode) {
m_sttEnabled = sttNode->getBool("enabled", true);
m_language = sttNode->getString("language", "fr");
} else {
// Fallback to flat format for tests
m_sttEnabled = configNode.getBool("sttEnabled", true);
m_language = configNode.getString("language", "fr");
}
// Subscribe to topics

View File

@ -144,6 +144,18 @@ TEST_CASE("TI_AI_005_HyperfocusAlertGeneratesSuggestion", "[ai][integration]") {
});
f.process();
// Verify LLM request published
REQUIRE(f.io.wasPublished("llm:request"));
auto req = f.io.getLastPublished("llm:request");
std::string convId = req["conversationId"];
// Simulate LLM response
f.io.injectMessage("llm:response", {
{"text", "Time to take a break!"},
{"conversationId", convId}
});
f.process();
// Verify suggestion published
REQUIRE(f.io.wasPublished("ai:suggestion"));
auto msg = f.io.getLastPublished("ai:suggestion");
@ -164,6 +176,18 @@ TEST_CASE("TI_AI_006_BreakReminderGeneratesSuggestion", "[ai][integration]") {
});
f.process();
// Verify LLM request published
REQUIRE(f.io.wasPublished("llm:request"));
auto req = f.io.getLastPublished("llm:request");
std::string convId = req["conversationId"];
// Simulate LLM response
f.io.injectMessage("llm:response", {
{"text", "Take a short break now!"},
{"conversationId", convId}
});
f.process();
// Verify suggestion
REQUIRE(f.io.wasPublished("ai:suggestion"));
}