feat(agent): orchestration v3 — surface MCP model-agnostic (M0→M4) — §14.3
Expose l'orchestration IdeA comme serveur MCP par-dessus le même OrchestratorService::dispatch, avec repli fichier .ideai/requests pour les CLI sans MCP. v3 réduite à la surface MCP : la messagerie inter-agents et la corrélation requête↔réponse étaient déjà résolues par §17 (send_blocking). - M0 capacité MCP sur le profil (McpCapability/McpConfigStrategy/McpTransport) - M1 injection conf MCP au LaunchAgent + prose adaptée selon la surface - M2 serveur/adapter MCP (JSON-RPC 2.0 maison ; outils idea_*) + ListAgents - M3 câblage par projet (registre mcp_servers jumeau du watcher) - M4 observabilité UI : OrchestratorRequestProcessed.source = file|mcp + badge Trois portes d'entrée (fichier, MCP, UI) → un seul dispatch ; aucun nouveau port applicatif ; MCP confiné à l'adapter infra. Tous lots verts (cycle §3). Cadrage : .ideai/briefs/orchestration-v3-cadrage.md ; ARCHITECTURE.md §14.3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -47,6 +47,14 @@ fn has_watcher(state: &AppState, id: &ProjectId) -> bool {
|
||||
state.orchestrator_watchers.lock().unwrap().contains_key(id)
|
||||
}
|
||||
|
||||
fn mcp_count(state: &AppState) -> usize {
|
||||
state.mcp_servers.lock().unwrap().len()
|
||||
}
|
||||
|
||||
fn has_mcp(state: &AppState, id: &ProjectId) -> bool {
|
||||
state.mcp_servers.lock().unwrap().contains_key(id)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_watch_registers_a_watcher_and_is_idempotent() {
|
||||
let state = AppState::build(temp_path("appdata"));
|
||||
@ -101,3 +109,105 @@ async fn watchers_are_isolated_per_project() {
|
||||
assert!(has_watcher(&state, &b.id));
|
||||
assert_eq!(watcher_count(&state), 1);
|
||||
}
|
||||
|
||||
// --- M3: IdeA MCP server lifecycle (twin of the watcher, Décision 4) ---
|
||||
//
|
||||
// The MCP server registry (`mcp_servers`) is the twin of `orchestrator_watchers`:
|
||||
// `ensure_orchestrator_watch` starts both side by side on open/create, and
|
||||
// `stop_orchestrator_watch` tears both down on close. These tests mirror the
|
||||
// watcher lifecycle tests above against the MCP registry. The per-project
|
||||
// supervision task parks on a stop signal (no blocking serve loop), so open/close
|
||||
// must return promptly — the `#[tokio::test]` harness itself proves no figing
|
||||
// (the test completes).
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_watch_starts_an_mcp_server_per_project() {
|
||||
let state = AppState::build(temp_path("appdata"));
|
||||
let project = make_project();
|
||||
|
||||
assert_eq!(mcp_count(&state), 0, "no MCP server before open");
|
||||
|
||||
state.ensure_orchestrator_watch(&project);
|
||||
assert!(
|
||||
has_mcp(&state, &project.id),
|
||||
"MCP server registered alongside the watcher"
|
||||
);
|
||||
assert_eq!(mcp_count(&state), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_mcp_server_is_idempotent_per_project() {
|
||||
let state = AppState::build(temp_path("appdata"));
|
||||
let project = make_project();
|
||||
|
||||
state.ensure_orchestrator_watch(&project);
|
||||
assert_eq!(mcp_count(&state), 1);
|
||||
|
||||
// Opening the same project again must not spawn a second MCP server.
|
||||
state.ensure_orchestrator_watch(&project);
|
||||
assert_eq!(
|
||||
mcp_count(&state),
|
||||
1,
|
||||
"MCP server start is idempotent per project"
|
||||
);
|
||||
assert!(has_mcp(&state, &project.id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stop_watch_unregisters_the_mcp_server() {
|
||||
let state = AppState::build(temp_path("appdata"));
|
||||
let project = make_project();
|
||||
|
||||
state.ensure_orchestrator_watch(&project);
|
||||
assert!(has_mcp(&state, &project.id));
|
||||
|
||||
state.stop_orchestrator_watch(&project.id);
|
||||
assert!(
|
||||
!has_mcp(&state, &project.id),
|
||||
"MCP server removed on close"
|
||||
);
|
||||
assert_eq!(mcp_count(&state), 0);
|
||||
|
||||
// Stopping an unknown project is a no-op (does not panic) for the MCP twin too.
|
||||
state.stop_orchestrator_watch(&project.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watcher_and_mcp_server_coexist_and_close_together() {
|
||||
let state = AppState::build(temp_path("appdata"));
|
||||
let project = make_project();
|
||||
|
||||
// Open: both entry doors onto the same OrchestratorService are live.
|
||||
state.ensure_orchestrator_watch(&project);
|
||||
assert!(has_watcher(&state, &project.id), "watcher live on open");
|
||||
assert!(has_mcp(&state, &project.id), "MCP server live on open");
|
||||
assert_eq!(watcher_count(&state), 1);
|
||||
assert_eq!(mcp_count(&state), 1);
|
||||
|
||||
// Close: the symmetric teardown removes both.
|
||||
state.stop_orchestrator_watch(&project.id);
|
||||
assert!(!has_watcher(&state, &project.id), "watcher gone on close");
|
||||
assert!(!has_mcp(&state, &project.id), "MCP server gone on close");
|
||||
assert_eq!(watcher_count(&state), 0);
|
||||
assert_eq!(mcp_count(&state), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_servers_are_isolated_per_project() {
|
||||
let state = AppState::build(temp_path("appdata"));
|
||||
let a = make_project();
|
||||
let b = make_project();
|
||||
|
||||
state.ensure_orchestrator_watch(&a);
|
||||
state.ensure_orchestrator_watch(&b);
|
||||
|
||||
assert_eq!(mcp_count(&state), 2, "one MCP server per open project");
|
||||
assert!(has_mcp(&state, &a.id));
|
||||
assert!(has_mcp(&state, &b.id));
|
||||
|
||||
// Closing one leaves the other's MCP server running.
|
||||
state.stop_orchestrator_watch(&a.id);
|
||||
assert!(!has_mcp(&state, &a.id));
|
||||
assert!(has_mcp(&state, &b.id));
|
||||
assert_eq!(mcp_count(&state), 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user