#include #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace aissia::testing { /** * @brief Test complet de la boucle conversationnelle end-to-end * * Scénario: * 1. Utilisateur (voice): "Prends note que j'aime le C++" * 2. AI → LLM (appelle tool storage_save_note) * 3. Storage sauvegarde dans .md * 4. Utilisateur (voice): "Qu'est-ce que j'aime ?" * 5. AI → LLM (appelle tool storage_query_notes) * 6. Storage récupère la note * 7. LLM répond avec le contenu * 8. Voice reçoit la réponse * * Ce test valide: * - Communication Voice → AI → LLM * - Exécution des tools MCP (storage_save_note, storage_query_notes) * - Persistence et retrieval de données * - Cohérence de la conversation multi-tour */ class IT_009_FullConversationLoop : public ITestModule { public: std::string getTestName() const override { return "IT_009_FullConversationLoop"; } std::string getDescription() const override { return "Test end-to-end full conversation loop"; } void setConfiguration(const grove::IDataNode& config, grove::IIO* io, grove::ITaskScheduler* scheduler) override { m_io = io; m_scheduler = scheduler; m_timeout = config.getInt("timeoutMs", 60000); // 60s for full loop grove::SubscriptionConfig subConfig; m_io->subscribe("llm:response", subConfig); m_io->subscribe("llm:error", subConfig); spdlog::info("[{}] Configured with timeout={}ms", getTestName(), m_timeout); } void process(const grove::IDataNode& input) override {} void shutdown() override {} const grove::IDataNode& getConfiguration() override { static grove::JsonDataNode config("config"); return config; } std::unique_ptr getHealthStatus() override { auto status = std::make_unique("health"); status->setString("status", "healthy"); return status; } std::unique_ptr getState() override { return std::make_unique("state"); } void setState(const grove::IDataNode& state) override {} std::string getType() const override { return "IT_009_FullConversationLoop"; } int getVersion() const override { return 1; } bool isIdle() const override { return true; } TestResult execute() override { auto start = std::chrono::steady_clock::now(); TestResult result; result.testName = getTestName(); const std::string testContent = "J'aime le C++ et GroveEngine"; const std::string conversationId = "it009_fullloop"; try { spdlog::info("[{}] ========== STEP 1: Save Note ==========", getTestName()); // Step 1: Ask AI to save a note auto request1 = std::make_unique("request"); request1->setString("query", "Utilise le tool storage_save_note pour sauvegarder cette information: '" + testContent + "'"); request1->setString("conversationId", conversationId); m_io->publish("ai:query", std::move(request1)); // Wait for save confirmation auto response1 = waitForMessage("llm:response", m_timeout); if (!response1) { auto error = waitForMessage("llm:error", 1000); if (error) { result.passed = false; result.message = "Step 1 failed - LLM error: " + error->getString("message", "Unknown"); } else { result.passed = false; result.message = "Step 1 failed - Timeout waiting for save confirmation"; } return result; } std::string saveResponse = response1->getString("text", ""); spdlog::info("[{}] Save response: {}", getTestName(), saveResponse); // Give time for async storage std::this_thread::sleep_for(std::chrono::milliseconds(2000)); // Verify note was saved bool noteFound = false; std::string foundPath; if (fs::exists("data/notes")) { for (const auto& entry : fs::recursive_directory_iterator("data/notes")) { if (entry.is_regular_file() && entry.path().extension() == ".md") { std::ifstream file(entry.path()); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); if (content.find("C++") != std::string::npos && content.find("GroveEngine") != std::string::npos) { noteFound = true; foundPath = entry.path().string(); file.close(); break; } file.close(); } } } if (!noteFound) { result.passed = false; result.message = "Step 1 failed - Note not saved to storage"; result.details["saveResponse"] = saveResponse; return result; } spdlog::info("[{}] ✅ Step 1 passed - Note saved: {}", getTestName(), foundPath); result.details["step1_notePath"] = foundPath; spdlog::info("[{}] ========== STEP 2: Query Note ==========", getTestName()); // Step 2: Ask AI to retrieve the note auto request2 = std::make_unique("request"); request2->setString("query", "Utilise le tool storage_query_notes pour chercher mes notes contenant 'C++'. " "Dis-moi ce que tu trouves."); request2->setString("conversationId", conversationId); m_io->publish("ai:query", std::move(request2)); // Wait for retrieval response auto response2 = waitForMessage("llm:response", m_timeout); if (!response2) { // Cleanup before failing if (!foundPath.empty() && fs::exists(foundPath)) { fs::remove(foundPath); } auto error = waitForMessage("llm:error", 1000); if (error) { result.passed = false; result.message = "Step 2 failed - LLM error: " + error->getString("message", "Unknown"); } else { result.passed = false; result.message = "Step 2 failed - Timeout waiting for query response"; } return result; } std::string queryResponse = response2->getString("text", ""); spdlog::info("[{}] Query response: {}", getTestName(), queryResponse); // Verify response contains our saved content bool contentRetrieved = queryResponse.find("C++") != std::string::npos && (queryResponse.find("GroveEngine") != std::string::npos || queryResponse.find("aime") != std::string::npos); if (!contentRetrieved) { result.passed = false; result.message = "Step 2 failed - Retrieved note doesn't match saved content"; result.details["saveResponse"] = saveResponse; result.details["queryResponse"] = queryResponse; // Cleanup if (!foundPath.empty() && fs::exists(foundPath)) { fs::remove(foundPath); } return result; } spdlog::info("[{}] ✅ Step 2 passed - Note retrieved correctly", getTestName()); // Step 3: Verify conversation continuity std::string convId1 = response1->getString("conversationId", ""); std::string convId2 = response2->getString("conversationId", ""); if (convId1 != conversationId || convId2 != conversationId) { result.passed = false; result.message = "Step 3 failed - Conversation ID mismatch"; result.details["expectedConvId"] = conversationId; result.details["convId1"] = convId1; result.details["convId2"] = convId2; // Cleanup if (!foundPath.empty() && fs::exists(foundPath)) { fs::remove(foundPath); } return result; } spdlog::info("[{}] ✅ Step 3 passed - Conversation continuity maintained", getTestName()); // SUCCESS - All steps passed result.passed = true; result.message = "Full conversation loop completed successfully"; result.details["step1_saveResponse"] = saveResponse; result.details["step2_queryResponse"] = queryResponse; result.details["conversationId"] = conversationId; result.details["notePath"] = foundPath; // Cleanup if (!foundPath.empty() && fs::exists(foundPath)) { fs::remove(foundPath); spdlog::info("[{}] Cleaned up test note: {}", getTestName(), foundPath); } } catch (const std::exception& e) { result.passed = false; result.message = std::string("Exception: ") + e.what(); spdlog::error("[{}] {}", getTestName(), result.message); } auto end = std::chrono::steady_clock::now(); result.durationMs = std::chrono::duration_cast( end - start).count(); spdlog::info("[{}] Test completed in {:.1f}s", getTestName(), result.durationMs / 1000.0); return result; } private: std::unique_ptr waitForMessage( const std::string& topic, int timeoutMs) { auto start = std::chrono::steady_clock::now(); while (true) { if (m_io->hasMessages() > 0) { auto msg = m_io->pullMessage(); if (msg.topic == topic && msg.data) { return std::move(msg.data); } } auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - start).count(); if (elapsed > timeoutMs) { return nullptr; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } grove::IIO* m_io = nullptr; grove::ITaskScheduler* m_scheduler = nullptr; int m_timeout = 60000; }; } // namespace aissia::testing extern "C" { grove::IModule* createModule() { return new aissia::testing::IT_009_FullConversationLoop(); } void destroyModule(grove::IModule* module) { delete module; } }