/** * @file VoiceModuleTests.cpp * @brief Integration tests for VoiceModule (10 TI) */ #include #include "mocks/MockIO.hpp" #include "utils/TimeSimulator.hpp" #include "utils/TestHelpers.hpp" #include "modules/VoiceModule.h" #include using namespace aissia; using namespace aissia::tests; // ============================================================================ // Test Fixture // ============================================================================ class VoiceTestFixture { public: MockIO io; TimeSimulator time; VoiceModule module; void configure(const json& config = json::object()) { json fullConfig = { {"ttsEnabled", true}, {"sttEnabled", true}, {"language", "fr"} }; fullConfig.merge_patch(config); grove::JsonDataNode configNode(fullConfig); module.setConfiguration(configNode, &io, nullptr); } void process() { grove::JsonDataNode input(time.createInput()); module.process(input); } }; // ============================================================================ // TI_VOICE_001: AI Response Triggers Speak // ============================================================================ TEST_CASE("TI_VOICE_001_AIResponseTriggersSpeak", "[voice][integration]") { VoiceTestFixture f; f.configure(); // Receive AI response f.io.injectMessage("ai:response", { {"text", "Voici la reponse a ta question"} }); f.process(); // Verify speak request REQUIRE(f.io.wasPublished("voice:speak")); auto msg = f.io.getLastPublished("voice:speak"); REQUIRE(msg["text"] == "Voici la reponse a ta question"); } // ============================================================================ // TI_VOICE_002: Suggestion Priority Speak // ============================================================================ TEST_CASE("TI_VOICE_002_SuggestionPrioritySpeak", "[voice][integration]") { VoiceTestFixture f; f.configure(); // Receive suggestion (should be priority) f.io.injectMessage("ai:suggestion", { {"message", "Tu devrais faire une pause"}, {"duration", 5} }); f.process(); // Verify speak with priority REQUIRE(f.io.wasPublished("voice:speak")); auto msg = f.io.getLastPublished("voice:speak"); REQUIRE(msg["priority"] == true); } // ============================================================================ // TI_VOICE_003: Speaking Started Updates State // ============================================================================ TEST_CASE("TI_VOICE_003_SpeakingStartedUpdatesState", "[voice][integration]") { VoiceTestFixture f; f.configure(); // Initially idle REQUIRE(f.module.isIdle() == true); // Receive speaking started f.io.injectMessage("voice:speaking_started", {{"text", "Hello"}}); f.process(); // Should be speaking REQUIRE(f.module.isIdle() == false); } // ============================================================================ // TI_VOICE_004: Speaking Ended Updates State // ============================================================================ TEST_CASE("TI_VOICE_004_SpeakingEndedUpdatesState", "[voice][integration]") { VoiceTestFixture f; f.configure(); // Start speaking f.io.injectMessage("voice:speaking_started", {{"text", "Hello"}}); f.process(); REQUIRE(f.module.isIdle() == false); // End speaking f.io.injectMessage("voice:speaking_ended", {}); f.process(); // Should be idle REQUIRE(f.module.isIdle() == true); } // ============================================================================ // TI_VOICE_005: IsIdle Reflects Speaking // ============================================================================ TEST_CASE("TI_VOICE_005_IsIdleReflectsSpeaking", "[voice][integration]") { VoiceTestFixture f; f.configure(); // Not speaking = idle REQUIRE(f.module.isIdle() == true); // Start speaking f.io.injectMessage("voice:speaking_started", {}); f.process(); REQUIRE(f.module.isIdle() == false); // Stop speaking f.io.injectMessage("voice:speaking_ended", {}); f.process(); REQUIRE(f.module.isIdle() == true); } // ============================================================================ // TI_VOICE_006: Transcription Forwarded (No Re-publish) // ============================================================================ TEST_CASE("TI_VOICE_006_TranscriptionForwarded", "[voice][integration]") { VoiceTestFixture f; f.configure(); // Receive transcription f.io.injectMessage("voice:transcription", { {"text", "Test transcription"}, {"confidence", 0.9} }); f.process(); // VoiceModule should NOT re-publish transcription // It just updates internal state REQUIRE(f.io.countPublished("voice:transcription") == 0); } // ============================================================================ // TI_VOICE_007: Total Spoken Incremented // ============================================================================ TEST_CASE("TI_VOICE_007_TotalSpokenIncremented", "[voice][integration]") { VoiceTestFixture f; f.configure(); // Complete one speak cycle f.io.injectMessage("voice:speaking_started", {}); f.process(); f.io.injectMessage("voice:speaking_ended", {}); f.process(); // Complete another f.io.injectMessage("voice:speaking_started", {}); f.process(); f.io.injectMessage("voice:speaking_ended", {}); f.process(); // Verify counter auto state = f.module.getState(); // TODO: Verify totalSpoken == 2 SUCCEED(); // Placeholder } // ============================================================================ // TI_VOICE_008: TTS Disabled Config // ============================================================================ TEST_CASE("TI_VOICE_008_TTSDisabledConfig", "[voice][integration]") { VoiceTestFixture f; f.configure({{"ttsEnabled", false}}); // Try to trigger speak f.io.injectMessage("ai:response", {{"text", "Should not speak"}}); f.process(); // Should NOT publish speak request REQUIRE(f.io.wasPublished("voice:speak") == false); } // ============================================================================ // TI_VOICE_009: Tool Command Speak // ============================================================================ TEST_CASE("TI_VOICE_009_ToolCommandSpeak", "[voice][integration]") { VoiceTestFixture f; f.configure(); // Send speak command via tool f.io.injectMessage("voice:command", { {"action", "speak"}, {"text", "Hello from tool"} }); f.process(); // Verify speak published REQUIRE(f.io.wasPublished("voice:speak")); auto msg = f.io.getLastPublished("voice:speak"); REQUIRE(msg["text"] == "Hello from tool"); } // ============================================================================ // TI_VOICE_010: State Serialization // ============================================================================ TEST_CASE("TI_VOICE_010_StateSerialization", "[voice][integration]") { VoiceTestFixture f; f.configure(); // Build state f.io.injectMessage("voice:speaking_started", {}); f.process(); f.io.injectMessage("voice:speaking_ended", {}); f.process(); // Get state auto state = f.module.getState(); REQUIRE(state != nullptr); // Restore VoiceModule module2; grove::JsonDataNode configNode(json::object()); module2.setConfiguration(configNode, &f.io, nullptr); module2.setState(*state); auto state2 = module2.getState(); REQUIRE(state2 != nullptr); SUCCEED(); // Placeholder }