734 lines
27 KiB
Markdown
734 lines
27 KiB
Markdown
# Plan d'Implementation - Tests d'Integration AISSIA
|
|
|
|
## Vue d'Ensemble
|
|
|
|
Ce document decrit le plan complet pour implementer 110 tests d'integration (TI) :
|
|
- 10 TI par module (6 modules = 60 TI)
|
|
- 50 TI pour le systeme MCP
|
|
|
|
## Architecture des Tests
|
|
|
|
```
|
|
tests/
|
|
├── CMakeLists.txt # Configuration tests
|
|
├── main.cpp # Entry point Catch2
|
|
├── mocks/
|
|
│ ├── MockIO.hpp # Mock IIO pub/sub
|
|
│ ├── MockDataNode.hpp # Mock IDataNode
|
|
│ ├── MockTaskScheduler.hpp # Mock ITaskScheduler
|
|
│ ├── MockTransport.hpp # Mock IMCPTransport
|
|
│ └── MockLLMProvider.hpp # Mock ILLMProvider
|
|
├── utils/
|
|
│ ├── TestHelpers.hpp # Utilitaires communs
|
|
│ ├── MessageCapture.hpp # Capture messages IIO
|
|
│ └── TimeSimulator.hpp # Simulation temps (gameTime)
|
|
├── modules/
|
|
│ ├── SchedulerModuleTests.cpp # 10 TI
|
|
│ ├── NotificationModuleTests.cpp # 10 TI
|
|
│ ├── MonitoringModuleTests.cpp # 10 TI
|
|
│ ├── AIModuleTests.cpp # 10 TI
|
|
│ ├── VoiceModuleTests.cpp # 10 TI
|
|
│ └── StorageModuleTests.cpp # 10 TI
|
|
└── mcp/
|
|
├── MCPTypesTests.cpp # 15 TI
|
|
├── StdioTransportTests.cpp # 20 TI
|
|
└── MCPClientTests.cpp # 15 TI
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 1: Infrastructure (Priorite Haute)
|
|
|
|
### 1.1 Mocks Essentiels
|
|
|
|
#### MockIO.hpp
|
|
```cpp
|
|
class MockIO : public grove::IIO {
|
|
public:
|
|
// Capture des messages publies
|
|
std::vector<std::pair<std::string, json>> publishedMessages;
|
|
|
|
// Queue de messages a recevoir
|
|
std::queue<grove::Message> incomingMessages;
|
|
|
|
void publish(const std::string& topic, const grove::IDataNode& data) override;
|
|
bool hasMessages() const override;
|
|
grove::Message popMessage() override;
|
|
|
|
// Helpers de test
|
|
void injectMessage(const std::string& topic, const json& data);
|
|
bool wasPublished(const std::string& topic) const;
|
|
json getLastPublished(const std::string& topic) const;
|
|
void clear();
|
|
};
|
|
```
|
|
|
|
#### MockTransport.hpp
|
|
```cpp
|
|
class MockTransport : public aissia::mcp::IMCPTransport {
|
|
public:
|
|
bool m_running = false;
|
|
std::vector<JsonRpcRequest> sentRequests;
|
|
std::queue<JsonRpcResponse> preparedResponses;
|
|
|
|
bool start() override;
|
|
void stop() override;
|
|
bool isRunning() const override;
|
|
JsonRpcResponse sendRequest(const JsonRpcRequest& request, int timeoutMs) override;
|
|
|
|
// Test helpers
|
|
void prepareResponse(const JsonRpcResponse& response);
|
|
void setStartFailure(bool fail);
|
|
};
|
|
```
|
|
|
|
### 1.2 Utilitaires
|
|
|
|
#### TimeSimulator.hpp
|
|
```cpp
|
|
class TimeSimulator {
|
|
public:
|
|
float m_gameTime = 0.0f;
|
|
|
|
json createInput(float deltaTime = 0.1f);
|
|
void advance(float seconds);
|
|
void setTime(float time);
|
|
};
|
|
```
|
|
|
|
#### MessageCapture.hpp
|
|
```cpp
|
|
class MessageCapture {
|
|
public:
|
|
void captureFrom(MockIO& io);
|
|
bool waitForMessage(const std::string& topic, int timeoutMs = 1000);
|
|
json getMessage(const std::string& topic);
|
|
int countMessages(const std::string& topic);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 2: Tests des Modules (60 TI)
|
|
|
|
### 2.1 SchedulerModule (10 TI)
|
|
|
|
| # | Test | Description | Topics Verifies |
|
|
|---|------|-------------|-----------------|
|
|
| 1 | `TI_SCHEDULER_001_StartTask` | Demarrer une tache publie `scheduler:task_started` | `scheduler:task_started` |
|
|
| 2 | `TI_SCHEDULER_002_CompleteTask` | Completer une tache publie `scheduler:task_completed` avec duree | `scheduler:task_completed` |
|
|
| 3 | `TI_SCHEDULER_003_HyperfocusDetection` | Session > 120min declenche `scheduler:hyperfocus_alert` | `scheduler:hyperfocus_alert` |
|
|
| 4 | `TI_SCHEDULER_004_HyperfocusAlertOnce` | Alerte hyperfocus envoyee une seule fois par session | single publish |
|
|
| 5 | `TI_SCHEDULER_005_BreakReminder` | Rappel de pause toutes les 45min | `scheduler:break_reminder` |
|
|
| 6 | `TI_SCHEDULER_006_IdlePausesSession` | Reception `monitoring:idle_detected` pause le tracking | state |
|
|
| 7 | `TI_SCHEDULER_007_ActivityResumesSession` | Reception `monitoring:activity_resumed` reprend le tracking | state |
|
|
| 8 | `TI_SCHEDULER_008_ToolQueryGetCurrentTask` | Query tool `get_current_task` retourne tache courante | `scheduler:response` |
|
|
| 9 | `TI_SCHEDULER_009_ToolCommandStartBreak` | Command tool `start_break` publie `scheduler:break_started` | `scheduler:break_started` |
|
|
| 10 | `TI_SCHEDULER_010_StateSerialization` | `getState()` et `setState()` preservent l'etat complet | state roundtrip |
|
|
|
|
**Implementation:**
|
|
```cpp
|
|
TEST_CASE("TI_SCHEDULER_001_StartTask", "[scheduler][integration]") {
|
|
MockIO io;
|
|
SchedulerModule module;
|
|
TimeSimulator time;
|
|
|
|
// Configure
|
|
json config = {{"hyperfocusThresholdMinutes", 120}};
|
|
module.setConfiguration(JsonDataNode(config), &io, nullptr);
|
|
|
|
// Add task
|
|
io.injectMessage("user:task_switch", {{"taskId", "task-1"}});
|
|
|
|
// Process
|
|
module.process(JsonDataNode(time.createInput()));
|
|
|
|
// Verify
|
|
REQUIRE(io.wasPublished("scheduler:task_started"));
|
|
auto msg = io.getLastPublished("scheduler:task_started");
|
|
REQUIRE(msg["taskId"] == "task-1");
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2.2 NotificationModule (10 TI)
|
|
|
|
| # | Test | Description | Topics Verifies |
|
|
|---|------|-------------|-----------------|
|
|
| 1 | `TI_NOTIF_001_QueueNotification` | `notify()` ajoute a la queue | state queue size |
|
|
| 2 | `TI_NOTIF_002_ProcessQueue` | `process()` traite max 3 notifications/frame | queue drain |
|
|
| 3 | `TI_NOTIF_003_PriorityOrdering` | URGENT traite avant NORMAL | order |
|
|
| 4 | `TI_NOTIF_004_SilentModeBlocksNonUrgent` | Mode silencieux bloque LOW/NORMAL/HIGH | filtering |
|
|
| 5 | `TI_NOTIF_005_SilentModeAllowsUrgent` | Mode silencieux laisse passer URGENT | filtering |
|
|
| 6 | `TI_NOTIF_006_MaxQueueSize` | Queue limitee a `maxQueueSize` (50) | overflow |
|
|
| 7 | `TI_NOTIF_007_LanguageConfig` | Langue configuree via `setConfiguration` | config |
|
|
| 8 | `TI_NOTIF_008_NotificationCountTracking` | Compteurs `notificationCount` et `urgentCount` | state |
|
|
| 9 | `TI_NOTIF_009_StateSerialization` | `getState()`/`setState()` preservent queue et compteurs | state roundtrip |
|
|
| 10 | `TI_NOTIF_010_MultipleFrameProcessing` | Queue > 3 elements necessite plusieurs frames | multi-frame |
|
|
|
|
**Implementation:**
|
|
```cpp
|
|
TEST_CASE("TI_NOTIF_004_SilentModeBlocksNonUrgent", "[notification][integration]") {
|
|
MockIO io;
|
|
NotificationModule module;
|
|
TimeSimulator time;
|
|
|
|
json config = {{"silentMode", true}};
|
|
module.setConfiguration(JsonDataNode(config), &io, nullptr);
|
|
|
|
module.notify("Test", "Normal message", NotificationModule::Priority::NORMAL);
|
|
module.notify("Test", "High message", NotificationModule::Priority::HIGH);
|
|
|
|
// State should show 0 pending (blocked)
|
|
auto state = module.getState();
|
|
REQUIRE(state->getInt("pendingCount") == 0);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2.3 MonitoringModule (10 TI)
|
|
|
|
| # | Test | Description | Topics Verifies |
|
|
|---|------|-------------|-----------------|
|
|
| 1 | `TI_MONITOR_001_AppChanged` | Reception `platform:window_changed` publie `monitoring:app_changed` | `monitoring:app_changed` |
|
|
| 2 | `TI_MONITOR_002_ProductiveAppClassification` | Apps dans `productiveApps` classees "productive" | classification |
|
|
| 3 | `TI_MONITOR_003_DistractingAppClassification` | Apps dans `distractingApps` classees "distracting" | classification |
|
|
| 4 | `TI_MONITOR_004_NeutralAppClassification` | Apps inconnues classees "neutral" | classification |
|
|
| 5 | `TI_MONITOR_005_DurationTracking` | `m_appDurations` accumule temps par app | duration map |
|
|
| 6 | `TI_MONITOR_006_IdleDetectedPausesTracking` | `platform:idle_detected` pause accumulation | state |
|
|
| 7 | `TI_MONITOR_007_ActivityResumedResumesTracking` | `platform:activity_resumed` reprend accumulation | state |
|
|
| 8 | `TI_MONITOR_008_ProductivityStats` | `m_totalProductiveSeconds` et `m_totalDistractingSeconds` corrects | stats |
|
|
| 9 | `TI_MONITOR_009_ToolQueryGetCurrentApp` | Query `get_current_app` retourne app courante | `monitoring:response` |
|
|
| 10 | `TI_MONITOR_010_StateSerialization` | `getState()`/`setState()` preservent stats et durations | state roundtrip |
|
|
|
|
**Implementation:**
|
|
```cpp
|
|
TEST_CASE("TI_MONITOR_002_ProductiveAppClassification", "[monitoring][integration]") {
|
|
MockIO io;
|
|
MonitoringModule module;
|
|
TimeSimulator time;
|
|
|
|
json config = {
|
|
{"productive_apps", {"Code", "CLion", "Visual Studio"}}
|
|
};
|
|
module.setConfiguration(JsonDataNode(config), &io, nullptr);
|
|
|
|
io.injectMessage("platform:window_changed", {
|
|
{"oldApp", ""},
|
|
{"newApp", "Code"},
|
|
{"duration", 0}
|
|
});
|
|
|
|
module.process(JsonDataNode(time.createInput()));
|
|
|
|
REQUIRE(io.wasPublished("monitoring:app_changed"));
|
|
auto msg = io.getLastPublished("monitoring:app_changed");
|
|
REQUIRE(msg["classification"] == "productive");
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2.4 AIModule (10 TI)
|
|
|
|
| # | Test | Description | Topics Verifies |
|
|
|---|------|-------------|-----------------|
|
|
| 1 | `TI_AI_001_QuerySendsLLMRequest` | Reception `ai:query` publie `llm:request` | `llm:request` |
|
|
| 2 | `TI_AI_002_VoiceTranscriptionTriggersQuery` | Reception `voice:transcription` envoie query | `llm:request` |
|
|
| 3 | `TI_AI_003_LLMResponseHandled` | Reception `llm:response` met `m_awaitingResponse = false` | state |
|
|
| 4 | `TI_AI_004_LLMErrorHandled` | Reception `llm:error` met `m_awaitingResponse = false` | state |
|
|
| 5 | `TI_AI_005_HyperfocusAlertGeneratesSuggestion` | `scheduler:hyperfocus_alert` publie `ai:suggestion` | `ai:suggestion` |
|
|
| 6 | `TI_AI_006_BreakReminderGeneratesSuggestion` | `scheduler:break_reminder` publie `ai:suggestion` | `ai:suggestion` |
|
|
| 7 | `TI_AI_007_SystemPromptInRequest` | `llm:request` contient `systemPrompt` de config | request content |
|
|
| 8 | `TI_AI_008_ConversationIdTracking` | Requetes utilisent `m_currentConversationId` | conversation |
|
|
| 9 | `TI_AI_009_TokenCountingAccumulates` | `m_totalTokens` s'accumule apres chaque reponse | state |
|
|
| 10 | `TI_AI_010_StateSerialization` | `getState()`/`setState()` preservent compteurs et conversation | state roundtrip |
|
|
|
|
**Implementation:**
|
|
```cpp
|
|
TEST_CASE("TI_AI_005_HyperfocusAlertGeneratesSuggestion", "[ai][integration]") {
|
|
MockIO io;
|
|
AIModule module;
|
|
TimeSimulator time;
|
|
|
|
json config = {{"system_prompt", "Tu es un assistant"}};
|
|
module.setConfiguration(JsonDataNode(config), &io, nullptr);
|
|
|
|
io.injectMessage("scheduler:hyperfocus_alert", {
|
|
{"sessionMinutes", 130},
|
|
{"task", "coding"}
|
|
});
|
|
|
|
module.process(JsonDataNode(time.createInput()));
|
|
|
|
REQUIRE(io.wasPublished("ai:suggestion"));
|
|
auto msg = io.getLastPublished("ai:suggestion");
|
|
REQUIRE(msg.contains("message"));
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2.5 VoiceModule (10 TI)
|
|
|
|
| # | Test | Description | Topics Verifies |
|
|
|---|------|-------------|-----------------|
|
|
| 1 | `TI_VOICE_001_AIResponseTriggersSpeak` | Reception `ai:response` publie `voice:speak` | `voice:speak` |
|
|
| 2 | `TI_VOICE_002_SuggestionPrioritySpeak` | Reception `ai:suggestion` publie `voice:speak` avec priorite | `voice:speak` priority |
|
|
| 3 | `TI_VOICE_003_SpeakingStartedUpdatesState` | `voice:speaking_started` met `m_isSpeaking = true` | state |
|
|
| 4 | `TI_VOICE_004_SpeakingEndedUpdatesState` | `voice:speaking_ended` met `m_isSpeaking = false` | state |
|
|
| 5 | `TI_VOICE_005_IsIdleReflectsSpeaking` | `isIdle()` retourne `!m_isSpeaking` | interface |
|
|
| 6 | `TI_VOICE_006_TranscriptionForwarded` | `voice:transcription` non traite par VoiceModule (forward only) | no re-publish |
|
|
| 7 | `TI_VOICE_007_TotalSpokenIncremented` | `m_totalSpoken` incremente apres chaque `speaking_ended` | counter |
|
|
| 8 | `TI_VOICE_008_TTSDisabledConfig` | Config `ttsEnabled: false` empeche `voice:speak` | config |
|
|
| 9 | `TI_VOICE_009_ToolCommandSpeak` | Command tool `speak` publie `voice:speak` | tool |
|
|
| 10 | `TI_VOICE_010_StateSerialization` | `getState()`/`setState()` preservent compteurs | state roundtrip |
|
|
|
|
**Implementation:**
|
|
```cpp
|
|
TEST_CASE("TI_VOICE_002_SuggestionPrioritySpeak", "[voice][integration]") {
|
|
MockIO io;
|
|
VoiceModule module;
|
|
TimeSimulator time;
|
|
|
|
json config = {{"ttsEnabled", true}};
|
|
module.setConfiguration(JsonDataNode(config), &io, nullptr);
|
|
|
|
io.injectMessage("ai:suggestion", {
|
|
{"message", "Tu devrais faire une pause"},
|
|
{"duration", 5}
|
|
});
|
|
|
|
module.process(JsonDataNode(time.createInput()));
|
|
|
|
REQUIRE(io.wasPublished("voice:speak"));
|
|
auto msg = io.getLastPublished("voice:speak");
|
|
REQUIRE(msg["priority"] == true);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2.6 StorageModule (10 TI)
|
|
|
|
| # | Test | Description | Topics Verifies |
|
|
|---|------|-------------|-----------------|
|
|
| 1 | `TI_STORAGE_001_TaskCompletedSavesSession` | `scheduler:task_completed` publie `storage:save_session` | `storage:save_session` |
|
|
| 2 | `TI_STORAGE_002_AppChangedSavesUsage` | `monitoring:app_changed` publie `storage:save_app_usage` | `storage:save_app_usage` |
|
|
| 3 | `TI_STORAGE_003_SessionSavedUpdatesLastId` | `storage:session_saved` met a jour `m_lastSessionId` | state |
|
|
| 4 | `TI_STORAGE_004_StorageErrorHandled` | `storage:error` log l'erreur sans crash | error handling |
|
|
| 5 | `TI_STORAGE_005_PendingSavesTracking` | `m_pendingSaves` incremente/decremente correctement | counter |
|
|
| 6 | `TI_STORAGE_006_TotalSavedTracking` | `m_totalSaved` s'accumule | counter |
|
|
| 7 | `TI_STORAGE_007_ToolQueryNotes` | Query `query_notes` retourne notes filtrees | `storage:response` |
|
|
| 8 | `TI_STORAGE_008_ToolCommandSaveNote` | Command `save_note` ajoute note a `m_notes` | state |
|
|
| 9 | `TI_STORAGE_009_NoteTagsFiltering` | Query notes avec tags filtre correctement | filtering |
|
|
| 10 | `TI_STORAGE_010_StateSerialization` | `getState()`/`setState()` preservent notes et compteurs | state roundtrip |
|
|
|
|
---
|
|
|
|
## Phase 3: Tests MCP (50 TI)
|
|
|
|
### 3.1 MCPTypes (15 TI)
|
|
|
|
| # | Test | Description |
|
|
|---|------|-------------|
|
|
| 1 | `TI_TYPES_001_MCPToolToJson` | `MCPTool::toJson()` serialise correctement |
|
|
| 2 | `TI_TYPES_002_MCPToolFromJson` | `MCPTool::fromJson()` deserialise correctement |
|
|
| 3 | `TI_TYPES_003_MCPToolFromJsonMissingFields` | `fromJson()` avec champs manquants utilise defauts |
|
|
| 4 | `TI_TYPES_004_MCPResourceFromJson` | `MCPResource::fromJson()` deserialise correctement |
|
|
| 5 | `TI_TYPES_005_MCPToolResultToJson` | `MCPToolResult::toJson()` serialise content et isError |
|
|
| 6 | `TI_TYPES_006_MCPCapabilitiesFromJson` | Detection correcte des capabilities |
|
|
| 7 | `TI_TYPES_007_MCPCapabilitiesEmpty` | Capabilities vides si pas de champs |
|
|
| 8 | `TI_TYPES_008_MCPServerInfoFromJson` | `MCPServerInfo::fromJson()` parse name/version/caps |
|
|
| 9 | `TI_TYPES_009_JsonRpcRequestToJson` | Serialisation avec/sans params |
|
|
| 10 | `TI_TYPES_010_JsonRpcResponseFromJson` | Parse result ou error |
|
|
| 11 | `TI_TYPES_011_JsonRpcResponseIsError` | `isError()` detecte presence de error |
|
|
| 12 | `TI_TYPES_012_MCPServerConfigFromJson` | Parse command, args, env, enabled |
|
|
| 13 | `TI_TYPES_013_MCPServerConfigEnvExpansion` | Variables env `${VAR}` expandees |
|
|
| 14 | `TI_TYPES_014_MCPServerConfigDisabled` | `enabled: false` honore |
|
|
| 15 | `TI_TYPES_015_JsonRpcRequestIdIncrement` | IDs uniques et croissants |
|
|
|
|
**Implementation:**
|
|
```cpp
|
|
TEST_CASE("TI_TYPES_001_MCPToolToJson", "[mcp][types]") {
|
|
MCPTool tool;
|
|
tool.name = "read_file";
|
|
tool.description = "Read a file";
|
|
tool.inputSchema = {{"type", "object"}, {"properties", {{"path", {{"type", "string"}}}}}};
|
|
|
|
json j = tool.toJson();
|
|
|
|
REQUIRE(j["name"] == "read_file");
|
|
REQUIRE(j["description"] == "Read a file");
|
|
REQUIRE(j["inputSchema"]["type"] == "object");
|
|
}
|
|
|
|
TEST_CASE("TI_TYPES_011_JsonRpcResponseIsError", "[mcp][types]") {
|
|
json errorJson = {
|
|
{"jsonrpc", "2.0"},
|
|
{"id", 1},
|
|
{"error", {{"code", -32600}, {"message", "Invalid Request"}}}
|
|
};
|
|
|
|
auto response = JsonRpcResponse::fromJson(errorJson);
|
|
|
|
REQUIRE(response.isError() == true);
|
|
REQUIRE(response.error.value()["code"] == -32600);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3.2 StdioTransport (20 TI)
|
|
|
|
| # | Test | Description |
|
|
|---|------|-------------|
|
|
| 1 | `TI_TRANSPORT_001_StartSpawnsProcess` | `start()` lance le processus enfant |
|
|
| 2 | `TI_TRANSPORT_002_StartFailsInvalidCommand` | `start()` retourne false si commande invalide |
|
|
| 3 | `TI_TRANSPORT_003_StopKillsProcess` | `stop()` termine le processus |
|
|
| 4 | `TI_TRANSPORT_004_IsRunningReflectsState` | `isRunning()` reflete l'etat reel |
|
|
| 5 | `TI_TRANSPORT_005_SendRequestWritesToStdin` | Request serialisee vers stdin |
|
|
| 6 | `TI_TRANSPORT_006_SendRequestReadsResponse` | Response lue depuis stdout |
|
|
| 7 | `TI_TRANSPORT_007_SendRequestTimeout` | Timeout si pas de reponse |
|
|
| 8 | `TI_TRANSPORT_008_SendRequestIdMatching` | Response matchee par ID |
|
|
| 9 | `TI_TRANSPORT_009_ConcurrentRequests` | Multiple requests simultanées OK |
|
|
| 10 | `TI_TRANSPORT_010_SendNotificationNoResponse` | Notification n'attend pas de reponse |
|
|
| 11 | `TI_TRANSPORT_011_ReaderThreadStartsOnStart` | Thread reader demarre avec `start()` |
|
|
| 12 | `TI_TRANSPORT_012_ReaderThreadStopsOnStop` | Thread reader s'arrete avec `stop()` |
|
|
| 13 | `TI_TRANSPORT_013_JsonParseErrorHandled` | JSON invalide n'crash pas |
|
|
| 14 | `TI_TRANSPORT_014_ProcessCrashDetected` | Crash process detecte rapidement |
|
|
| 15 | `TI_TRANSPORT_015_LargeMessageHandling` | Messages > 64KB transmis |
|
|
| 16 | `TI_TRANSPORT_016_MultilineJsonHandling` | JSON multiline traite correctement |
|
|
| 17 | `TI_TRANSPORT_017_EnvVariablesPassedToProcess` | Variables env transmises au processus |
|
|
| 18 | `TI_TRANSPORT_018_ArgsPassedToProcess` | Arguments CLI transmis |
|
|
| 19 | `TI_TRANSPORT_019_DestructorCleansUp` | Destructeur nettoie ressources |
|
|
| 20 | `TI_TRANSPORT_020_RestartAfterStop` | `start()` apres `stop()` fonctionne |
|
|
|
|
**Implementation (avec mock process):**
|
|
```cpp
|
|
// Pour les tests, on peut creer un mini-serveur echo en Python ou utiliser un mock
|
|
|
|
TEST_CASE("TI_TRANSPORT_007_SendRequestTimeout", "[mcp][transport]") {
|
|
MCPServerConfig config;
|
|
config.command = "cat"; // cat ne repond jamais au JSON-RPC
|
|
|
|
StdioTransport transport(config);
|
|
transport.start();
|
|
|
|
JsonRpcRequest request;
|
|
request.id = 1;
|
|
request.method = "test";
|
|
|
|
auto response = transport.sendRequest(request, 100); // 100ms timeout
|
|
|
|
REQUIRE(response.isError() == true);
|
|
transport.stop();
|
|
}
|
|
|
|
// Test avec echo server (Python script)
|
|
TEST_CASE("TI_TRANSPORT_006_SendRequestReadsResponse", "[mcp][transport]") {
|
|
MCPServerConfig config;
|
|
config.command = "python";
|
|
config.args = {"tests/fixtures/echo_server.py"};
|
|
|
|
StdioTransport transport(config);
|
|
REQUIRE(transport.start() == true);
|
|
|
|
JsonRpcRequest request;
|
|
request.id = 42;
|
|
request.method = "echo";
|
|
request.params = {{"message", "hello"}};
|
|
|
|
auto response = transport.sendRequest(request, 5000);
|
|
|
|
REQUIRE(response.isError() == false);
|
|
REQUIRE(response.id == 42);
|
|
transport.stop();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3.3 MCPClient (15 TI)
|
|
|
|
| # | Test | Description |
|
|
|---|------|-------------|
|
|
| 1 | `TI_CLIENT_001_LoadConfigValid` | `loadConfig()` parse mcp.json valide |
|
|
| 2 | `TI_CLIENT_002_LoadConfigInvalid` | `loadConfig()` gere fichier invalide |
|
|
| 3 | `TI_CLIENT_003_LoadConfigMissingFile` | `loadConfig()` retourne false si fichier absent |
|
|
| 4 | `TI_CLIENT_004_ConnectAllStartsServers` | `connectAll()` demarre tous les serveurs enabled |
|
|
| 5 | `TI_CLIENT_005_ConnectAllSkipsDisabled` | `connectAll()` skip les serveurs `enabled: false` |
|
|
| 6 | `TI_CLIENT_006_ConnectSingleServer` | `connect(name)` demarre un seul serveur |
|
|
| 7 | `TI_CLIENT_007_DisconnectSingleServer` | `disconnect(name)` arrete un serveur |
|
|
| 8 | `TI_CLIENT_008_DisconnectAllCleansUp` | `disconnectAll()` arrete tous les serveurs |
|
|
| 9 | `TI_CLIENT_009_ListAllToolsAggregates` | `listAllTools()` combine tools de tous serveurs |
|
|
| 10 | `TI_CLIENT_010_ToolNamePrefixed` | Tools prefixes par nom serveur (`server:tool`) |
|
|
| 11 | `TI_CLIENT_011_CallToolRoutesToServer` | `callTool()` route vers bon serveur |
|
|
| 12 | `TI_CLIENT_012_CallToolInvalidName` | `callTool()` avec nom invalide retourne erreur |
|
|
| 13 | `TI_CLIENT_013_CallToolDisconnectedServer` | `callTool()` sur serveur deconnecte retourne erreur |
|
|
| 14 | `TI_CLIENT_014_ToolCountAccurate` | `toolCount()` reflete nombre total de tools |
|
|
| 15 | `TI_CLIENT_015_IsConnectedAccurate` | `isConnected(name)` reflete etat reel |
|
|
|
|
**Implementation:**
|
|
```cpp
|
|
TEST_CASE("TI_CLIENT_010_ToolNamePrefixed", "[mcp][client]") {
|
|
MCPClient client;
|
|
|
|
// Utiliser mock transport
|
|
client.loadConfig("tests/fixtures/mock_mcp.json");
|
|
// Le mock simule un serveur avec tool "read_file"
|
|
|
|
client.connectAll();
|
|
auto tools = client.listAllTools();
|
|
|
|
bool hasPrefix = false;
|
|
for (const auto& tool : tools) {
|
|
if (tool.name.find(":") != std::string::npos) {
|
|
hasPrefix = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
REQUIRE(hasPrefix == true);
|
|
client.disconnectAll();
|
|
}
|
|
|
|
TEST_CASE("TI_CLIENT_012_CallToolInvalidName", "[mcp][client]") {
|
|
MCPClient client;
|
|
client.loadConfig("tests/fixtures/mock_mcp.json");
|
|
client.connectAll();
|
|
|
|
auto result = client.callTool("nonexistent:tool", {});
|
|
|
|
REQUIRE(result.isError == true);
|
|
client.disconnectAll();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 4: Fixtures et Scripts de Test
|
|
|
|
### 4.1 Echo Server MCP (Python)
|
|
`tests/fixtures/echo_server.py`
|
|
```python
|
|
#!/usr/bin/env python3
|
|
import json
|
|
import sys
|
|
|
|
def main():
|
|
while True:
|
|
line = sys.stdin.readline()
|
|
if not line:
|
|
break
|
|
try:
|
|
request = json.loads(line)
|
|
response = {
|
|
"jsonrpc": "2.0",
|
|
"id": request.get("id"),
|
|
"result": request.get("params", {})
|
|
}
|
|
sys.stdout.write(json.dumps(response) + "\n")
|
|
sys.stdout.flush()
|
|
except:
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
```
|
|
|
|
### 4.2 Mock MCP Server (pour tests d'integration)
|
|
`tests/fixtures/mock_mcp_server.py`
|
|
```python
|
|
#!/usr/bin/env python3
|
|
import json
|
|
import sys
|
|
|
|
TOOLS = [
|
|
{"name": "test_tool", "description": "A test tool", "inputSchema": {"type": "object"}}
|
|
]
|
|
|
|
def handle_request(request):
|
|
method = request.get("method")
|
|
|
|
if method == "initialize":
|
|
return {
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {"tools": {}},
|
|
"serverInfo": {"name": "MockServer", "version": "1.0"}
|
|
}
|
|
elif method == "tools/list":
|
|
return {"tools": TOOLS}
|
|
elif method == "tools/call":
|
|
return {"content": [{"type": "text", "text": "Tool executed"}]}
|
|
|
|
return {"error": {"code": -32601, "message": "Method not found"}}
|
|
|
|
def main():
|
|
while True:
|
|
line = sys.stdin.readline()
|
|
if not line:
|
|
break
|
|
try:
|
|
request = json.loads(line)
|
|
result = handle_request(request)
|
|
response = {"jsonrpc": "2.0", "id": request.get("id")}
|
|
if "error" in result:
|
|
response["error"] = result["error"]
|
|
else:
|
|
response["result"] = result
|
|
sys.stdout.write(json.dumps(response) + "\n")
|
|
sys.stdout.flush()
|
|
except Exception as e:
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
```
|
|
|
|
### 4.3 Config Mock MCP
|
|
`tests/fixtures/mock_mcp.json`
|
|
```json
|
|
{
|
|
"mock_server": {
|
|
"command": "python",
|
|
"args": ["tests/fixtures/mock_mcp_server.py"],
|
|
"enabled": true
|
|
},
|
|
"disabled_server": {
|
|
"command": "nonexistent",
|
|
"enabled": false
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5: CMakeLists.txt Tests
|
|
|
|
```cmake
|
|
# ============================================================================
|
|
# Tests d'Integration
|
|
# ============================================================================
|
|
|
|
# Fetch Catch2 if not already available
|
|
FetchContent_Declare(
|
|
Catch2
|
|
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
|
GIT_TAG v3.4.0
|
|
)
|
|
FetchContent_MakeAvailable(Catch2)
|
|
|
|
# Test executable
|
|
add_executable(aissia_tests
|
|
tests/main.cpp
|
|
|
|
# Mocks
|
|
tests/mocks/MockIO.cpp
|
|
|
|
# Module tests
|
|
tests/modules/SchedulerModuleTests.cpp
|
|
tests/modules/NotificationModuleTests.cpp
|
|
tests/modules/MonitoringModuleTests.cpp
|
|
tests/modules/AIModuleTests.cpp
|
|
tests/modules/VoiceModuleTests.cpp
|
|
tests/modules/StorageModuleTests.cpp
|
|
|
|
# MCP tests
|
|
tests/mcp/MCPTypesTests.cpp
|
|
tests/mcp/StdioTransportTests.cpp
|
|
tests/mcp/MCPClientTests.cpp
|
|
)
|
|
|
|
target_link_libraries(aissia_tests PRIVATE
|
|
Catch2::Catch2WithMain
|
|
GroveEngine::impl
|
|
AissiaTools
|
|
spdlog::spdlog
|
|
)
|
|
|
|
target_include_directories(aissia_tests PRIVATE
|
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
|
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
|
)
|
|
|
|
# Copy test fixtures
|
|
file(COPY tests/fixtures/ DESTINATION ${CMAKE_BINARY_DIR}/tests/fixtures)
|
|
|
|
# CTest integration
|
|
include(CTest)
|
|
include(Catch)
|
|
catch_discover_tests(aissia_tests)
|
|
|
|
# Custom target for running tests
|
|
add_custom_target(test_all
|
|
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
|
|
DEPENDS aissia_tests
|
|
COMMENT "Running all integration tests"
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Ordre d'Implementation Recommande
|
|
|
|
### Sprint 1: Infrastructure (2-3 jours)
|
|
1. Creer structure `tests/`
|
|
2. Implementer `MockIO.hpp`
|
|
3. Implementer `MockTransport.hpp`
|
|
4. Implementer `TimeSimulator.hpp`
|
|
5. Setup CMakeLists.txt tests
|
|
6. Creer fixtures Python
|
|
|
|
### Sprint 2: Tests MCPTypes (1 jour)
|
|
1. `TI_TYPES_001` a `TI_TYPES_015`
|
|
2. Valider serialisation/deserialisation
|
|
|
|
### Sprint 3: Tests StdioTransport (2 jours)
|
|
1. `TI_TRANSPORT_001` a `TI_TRANSPORT_010` (basic)
|
|
2. `TI_TRANSPORT_011` a `TI_TRANSPORT_020` (advanced)
|
|
|
|
### Sprint 4: Tests MCPClient (1 jour)
|
|
1. `TI_CLIENT_001` a `TI_CLIENT_015`
|
|
|
|
### Sprint 5: Tests Modules (3-4 jours)
|
|
1. SchedulerModule (10 TI)
|
|
2. NotificationModule (10 TI)
|
|
3. MonitoringModule (10 TI)
|
|
4. AIModule (10 TI)
|
|
5. VoiceModule (10 TI)
|
|
6. StorageModule (10 TI)
|
|
|
|
---
|
|
|
|
## Metriques de Succes
|
|
|
|
- [ ] 110 tests implementes
|
|
- [ ] Couverture > 80% pour chaque module
|
|
- [ ] Tous les tests passent en CI
|
|
- [ ] Temps d'execution < 30 secondes
|
|
- [ ] Aucune dependance externe (sauf Python pour mock MCP)
|
|
|
|
---
|
|
|
|
## Commandes de Build et Execution
|
|
|
|
```bash
|
|
# Build complet avec tests
|
|
cmake -B build -DBUILD_TESTING=ON
|
|
cmake --build build
|
|
|
|
# Executer tous les tests
|
|
cmake --build build --target test_all
|
|
|
|
# Executer tests specifiques
|
|
./build/aissia_tests "[scheduler]" # Tests scheduler
|
|
./build/aissia_tests "[mcp]" # Tests MCP
|
|
./build/aissia_tests "[integration]" # Tous les TI
|
|
|
|
# Avec verbose
|
|
./build/aissia_tests -s -d yes
|
|
```
|