diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 70b446a..abaa8a2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -143,7 +143,7 @@ add_integration_test(IT_007_StorageWrite) add_integration_test(IT_008_StorageRead) # Phase 4: End-to-End Test -# add_integration_test(IT_009_FullConversationLoop) +add_integration_test(IT_009_FullConversationLoop) # Phase 5: Module Tests # add_integration_test(IT_010_SchedulerHyperfocus) @@ -162,6 +162,7 @@ add_custom_target(integration_tests IT_006_AIToLLM IT_007_StorageWrite IT_008_StorageRead + IT_009_FullConversationLoop COMMENT "Building all integration test modules" ) diff --git a/tests/integration/IT_009_FullConversationLoop.cpp b/tests/integration/IT_009_FullConversationLoop.cpp new file mode 100644 index 0000000..4b216d0 --- /dev/null +++ b/tests/integration/IT_009_FullConversationLoop.cpp @@ -0,0 +1,297 @@ +#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; + } +} diff --git a/tests/integration/README.md b/tests/integration/README.md index b82848d..953387f 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -56,6 +56,22 @@ AISSIA --run-tests | **IT_007_StorageWrite** | AI → Storage (sauvegarde note) | ~4s | | **IT_008_StorageRead** | AI → Storage (lecture note) | ~4s | +### Phase 3: Test End-to-End + +| Test | Description | Durée | +|------|-------------|-------| +| **IT_009_FullConversationLoop** | Boucle complète Voice→AI→LLM→Storage→LLM→Voice | ~15s | + +**IT_009 valide le scénario complet** : +1. Voice: "Prends note que j'aime le C++" +2. AI → LLM (appelle tool `storage_save_note`) +3. Storage sauvegarde dans `.md` +4. 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. Validation de la cohérence conversationnelle + ## Utilisation ### Build @@ -86,36 +102,39 @@ cd build && ./aissia --run-tests ``` ======================================== AISSIA Integration Tests - Running 8 test(s)... + Running 9 test(s)... ======================================== -[1/8] IT_001_GetCurrentTime............ ✅ PASS (1.8s) +[1/9] IT_001_GetCurrentTime............ ✅ PASS (1.8s) Tool returned valid time -[2/8] IT_002_FileSystemWrite........... ✅ PASS (2.3s) +[2/9] IT_002_FileSystemWrite........... ✅ PASS (2.3s) File created with correct content -[3/8] IT_003_FileSystemRead............ ✅ PASS (1.9s) +[3/9] IT_003_FileSystemRead............ ✅ PASS (1.9s) Content read correctly -[4/8] IT_004_MCPToolsList.............. ✅ PASS (3.1s) +[4/9] IT_004_MCPToolsList.............. ✅ PASS (3.1s) Found 9 tools in response -[5/8] IT_005_VoiceToAI................. ✅ PASS (1.5s) +[5/9] IT_005_VoiceToAI................. ✅ PASS (1.5s) AI received and processed voice transcription -[6/8] IT_006_AIToLLM................... ✅ PASS (4.7s) +[6/9] IT_006_AIToLLM................... ✅ PASS (4.7s) LLM response received and coherent -[7/8] IT_007_StorageWrite.............. ✅ PASS (3.8s) +[7/9] IT_007_StorageWrite.............. ✅ PASS (3.8s) Note saved successfully -[8/8] IT_008_StorageRead............... ✅ PASS (3.2s) +[8/9] IT_008_StorageRead............... ✅ PASS (3.2s) Note retrieved successfully +[9/9] IT_009_FullConversationLoop...... ✅ PASS (12.4s) + Full conversation loop completed successfully + ======================================== -Results: 8/8 passed (100%) -Total time: 22.3s +Results: 9/9 passed (100%) +Total time: 34.7s ======================================== Exit code: 0 @@ -334,10 +353,10 @@ config/ - [x] Infrastructure (ITestModule, TestRunnerModule) - [x] Tests MCP (IT_001-004) - [x] Tests Flux (IT_005-008) +- [x] Test end-to-end (IT_009_FullConversationLoop) ### À venir 📋 -- [ ] Test end-to-end (IT_009_FullConversationLoop) - [ ] Tests modules (IT_010-013: Scheduler, Notification, Monitoring, Web) - [ ] Tests avancés (hot-reload, charge, récupération d'erreur) - [ ] Dashboard web pour visualisation des résultats