feat(plugins): hot reload des plugins sans relancer IdeA — use case ReloadPlugin (invalidation registry/catalogue, resultat hashes+changed), tools MCP d'administration plugin agent-facing (mcp/plugins.rs + PluginToolProvider), evenement domaine plugin_reloaded relaye sur le wire (dto/events), exposition openai_tools rafraichie, wiring app-tauri — #271/#269 (QA verte #273: application 32, mcp_server 38, openai_tools 10, plugin_install_load 5, plugin 10, wire 1 — 96 tests ciblés; app-tauri compile sans desktop runtime)
This commit is contained in:
@ -59,7 +59,8 @@ use application::{
|
||||
use infrastructure::orchestrator::mcp::jsonrpc::error_codes;
|
||||
use infrastructure::orchestrator::mcp::tools::classified_tool_names;
|
||||
use infrastructure::orchestrator::mcp::{
|
||||
TemplateToolError, TemplateToolProvider, TicketToolError, TicketToolProvider,
|
||||
PluginToolError, PluginToolProvider, TemplateToolError, TemplateToolProvider, TicketToolError,
|
||||
TicketToolProvider,
|
||||
};
|
||||
use infrastructure::{
|
||||
InMemoryConversationRegistry, InMemoryMailbox, McpServer, MediatedInbox, MemoryTransport,
|
||||
@ -631,6 +632,31 @@ struct FakeTemplateTools {
|
||||
mutation_attempts: Arc<Mutex<usize>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct FakePluginTools {
|
||||
calls: Arc<Mutex<Vec<(String, String, Value)>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PluginToolProvider for FakePluginTools {
|
||||
async fn handle_plugin_tool(
|
||||
&self,
|
||||
_project: &Project,
|
||||
requester: &str,
|
||||
name: &str,
|
||||
arguments: Value,
|
||||
) -> Result<Value, PluginToolError> {
|
||||
self.calls
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((requester.to_owned(), name.to_owned(), arguments.clone()));
|
||||
Ok(json!({
|
||||
"plugin": { "id": arguments["pluginId"] },
|
||||
"changed": true
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeTemplateTools {
|
||||
fn calls(&self) -> Vec<String> {
|
||||
self.calls.lock().unwrap().clone()
|
||||
@ -774,6 +800,8 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
"idea_template_create",
|
||||
"idea_template_update",
|
||||
"idea_template_delete",
|
||||
// Public plugin administration tools.
|
||||
"idea_plugin_reload",
|
||||
] {
|
||||
assert!(
|
||||
names.contains(&expected),
|
||||
@ -783,8 +811,8 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
assert!(!names.contains(&"idea_reply"));
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
47,
|
||||
"exactly the forty-seven exposed idea_* tools; got {names:?}"
|
||||
48,
|
||||
"exactly the forty-eight exposed idea_* tools; got {names:?}"
|
||||
);
|
||||
|
||||
// Every tool advertises an object input schema.
|
||||
@ -897,6 +925,7 @@ async fn requester_with_durable_store_but_no_override_sees_read_only_tools() {
|
||||
assert!(!names.contains(&"idea_template_create"));
|
||||
assert!(!names.contains(&"idea_template_update"));
|
||||
assert!(!names.contains(&"idea_template_delete"));
|
||||
assert!(!names.contains(&"idea_plugin_reload"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -1006,6 +1035,43 @@ async fn general_agent_without_mcp_override_is_read_only_for_tools_call() {
|
||||
assert_eq!(template_tools.mutation_attempts(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicitly_allowed_plugin_reload_routes_to_provider_with_camel_case_arguments() {
|
||||
let (service, _s) = build_service(FakeContexts::new());
|
||||
let plugin_tools = Arc::new(FakePluginTools::default());
|
||||
let agent = AgentId::from_uuid(Uuid::from_u128(271));
|
||||
let server = server_with_mcp_permissions(service, allow_doc(agent, &["idea_plugin_reload"]))
|
||||
.with_plugin_tools(plugin_tools.clone())
|
||||
.for_requester(agent.to_string());
|
||||
|
||||
let response = server
|
||||
.handle_raw(&tools_call(
|
||||
271,
|
||||
"idea_plugin_reload",
|
||||
json!({
|
||||
"pluginId": "dev.acme.gitgraph",
|
||||
"expectedContentHash": "abc123"
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.expect("reply owed");
|
||||
|
||||
assert!(
|
||||
response.error.is_none(),
|
||||
"transport error: {:?}",
|
||||
response.error
|
||||
);
|
||||
let result = response.result.expect("tool result");
|
||||
assert_eq!(result["isError"], json!(false), "got {result}");
|
||||
let payload: Value = serde_json::from_str(result_text(&result)).unwrap();
|
||||
assert_eq!(payload["plugin"]["id"], json!("dev.acme.gitgraph"));
|
||||
let calls = plugin_tools.calls.lock().unwrap();
|
||||
assert_eq!(calls.len(), 1);
|
||||
assert_eq!(calls[0].0, agent.to_string());
|
||||
assert_eq!(calls[0].1, "idea_plugin_reload");
|
||||
assert_eq!(calls[0].2["expectedContentHash"], json!("abc123"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_mcp_requester_with_durable_store_is_read_only() {
|
||||
let (service, _mailbox, _sessions) = build_service_with_mailbox(FakeContexts::new());
|
||||
|
||||
Reference in New Issue
Block a user