feat(wave): #119/#122/#131/#132 verts + sprint plugins ESM/persistance #135/#136/#139
État d'intégration confiné à la branche batch. Les tickets #119 (skills → capacités agent découvrables), #122 (override permissions par défaut), #131 (effort par agent/presets) et #132 (outil MCP d'édition du contexte projet) sont verts en périmètre. Le sprint plugins multi-fichiers ESM / persistance plugin-owned (#135/#136/#139) est co-implémenté dans les MÊMES fichiers de câblage (frontend/src/ports/index.ts, backend/src/lib.rs, domain/ports.rs, backend/dto.rs), inséparable sans staging interactif (indisponible ici). Commit unique volontaire : préserve l'état vert QA sans découpe hunk risquée. NON mergé vers develop tant que #137 (QA e2e plugins) n'est pas vert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -8,7 +8,7 @@ use application::{
|
||||
ListPlugins, UninstallPlugin, UninstallPluginInput,
|
||||
};
|
||||
use infrastructure::{
|
||||
ExternalMcpPluginSupervisor, FsPluginPackageStore, FsPluginRegistryStore,
|
||||
ExternalMcpPluginSupervisor, FsPluginPackageStore, FsPluginRegistryStore, FsPluginStorageStore,
|
||||
TokioBroadcastEventBus,
|
||||
};
|
||||
|
||||
@ -39,6 +39,48 @@ fn sdk_hello_plugin_path() -> PathBuf {
|
||||
.join("sdk/IdeaSDK/examples/hello-plugin")
|
||||
}
|
||||
|
||||
fn materialize_sdk_hello_plugin() -> PathBuf {
|
||||
let source = sdk_hello_plugin_path();
|
||||
let root = temp_dir("hello-plugin-built");
|
||||
fs::create_dir_all(root.join("dist")).unwrap();
|
||||
fs::write(
|
||||
root.join("idea-plugin.json"),
|
||||
fs::read(source.join("idea-plugin.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join("dist/index.js"),
|
||||
"export function activate() { return 'hello-plugin'; }",
|
||||
)
|
||||
.unwrap();
|
||||
root
|
||||
}
|
||||
|
||||
fn write_multifile_plugin(root: &Path) {
|
||||
fs::create_dir_all(root.join("dist/core")).unwrap();
|
||||
fs::write(
|
||||
root.join("idea-plugin.json"),
|
||||
r#"{
|
||||
"ideaPluginManifestVersion": 1,
|
||||
"id": "dev.acme.multifile",
|
||||
"displayName": "Multifile Plugin",
|
||||
"version": "0.1.0",
|
||||
"main": "dist/index.js",
|
||||
"trustLevel": "full",
|
||||
"capabilities": ["ui"],
|
||||
"contributes": {}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join("dist/index.js"),
|
||||
"import { answer } from './constants.js'; export default answer;",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(root.join("dist/constants.js"), "export const answer = 42;").unwrap();
|
||||
fs::write(root.join("dist/core/util.js"), "export const util = true;").unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn installs_reference_fixture_and_loads_runtime_catalog() {
|
||||
let app_data = temp_dir("app-data");
|
||||
@ -100,7 +142,7 @@ async fn installs_reference_fixture_and_loads_runtime_catalog() {
|
||||
#[tokio::test]
|
||||
async fn installs_sdk_hello_plugin_and_loads_runtime_catalog() {
|
||||
let app_data = temp_dir("hello-app-data");
|
||||
let hello_plugin = sdk_hello_plugin_path();
|
||||
let hello_plugin = materialize_sdk_hello_plugin();
|
||||
let packages = Arc::new(FsPluginPackageStore::new(&app_data));
|
||||
let registry = Arc::new(FsPluginRegistryStore::new(&app_data));
|
||||
let validator = Arc::new(JsonPluginManifestValidator::new("0.3.0"));
|
||||
@ -152,14 +194,16 @@ async fn installs_sdk_hello_plugin_and_loads_runtime_catalog() {
|
||||
assert_eq!(plugin.contributes.layouts[0].label.as_str(), "hello-world");
|
||||
|
||||
let _ = fs::remove_dir_all(app_data);
|
||||
let _ = fs::remove_dir_all(hello_plugin);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn uninstalls_then_reinstalls_sdk_hello_plugin_without_runtime_residue() {
|
||||
let app_data = temp_dir("hello-reinstall-app-data");
|
||||
let hello_plugin = sdk_hello_plugin_path();
|
||||
let hello_plugin = materialize_sdk_hello_plugin();
|
||||
let packages = Arc::new(FsPluginPackageStore::new(&app_data));
|
||||
let registry = Arc::new(FsPluginRegistryStore::new(&app_data));
|
||||
let storage = Arc::new(FsPluginStorageStore::new(&app_data));
|
||||
let validator = Arc::new(JsonPluginManifestValidator::new("0.3.0"));
|
||||
let events = Arc::new(TokioBroadcastEventBus::new());
|
||||
let mcp = Arc::new(ExternalMcpPluginSupervisor::new());
|
||||
@ -170,7 +214,7 @@ async fn uninstalls_then_reinstalls_sdk_hello_plugin_without_runtime_residue() {
|
||||
events.clone(),
|
||||
mcp.clone(),
|
||||
);
|
||||
let uninstall = UninstallPlugin::new(packages.clone(), registry.clone(), events, mcp);
|
||||
let uninstall = UninstallPlugin::new(packages.clone(), storage, registry.clone(), events, mcp);
|
||||
|
||||
install
|
||||
.execute(hello_plugin.to_string_lossy().into_owned())
|
||||
@ -225,6 +269,76 @@ async fn uninstalls_then_reinstalls_sdk_hello_plugin_without_runtime_residue() {
|
||||
assert!(catalog.plugins[0].bundle_url.ends_with("/dist/index.js"));
|
||||
|
||||
let _ = fs::remove_dir_all(app_data);
|
||||
let _ = fs::remove_dir_all(hello_plugin);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn uninstall_multifile_plugin_removes_package_registry_and_runtime_residue() {
|
||||
let app_data = temp_dir("multifile-uninstall-app-data");
|
||||
let source = temp_dir("multifile-source");
|
||||
write_multifile_plugin(&source);
|
||||
let packages = Arc::new(FsPluginPackageStore::new(&app_data));
|
||||
let registry = Arc::new(FsPluginRegistryStore::new(&app_data));
|
||||
let storage = Arc::new(FsPluginStorageStore::new(&app_data));
|
||||
let validator = Arc::new(JsonPluginManifestValidator::new("0.3.0"));
|
||||
let events = Arc::new(TokioBroadcastEventBus::new());
|
||||
let mcp = Arc::new(ExternalMcpPluginSupervisor::new());
|
||||
let install = InstallPluginFromDirectory::new(
|
||||
packages.clone(),
|
||||
registry.clone(),
|
||||
validator.clone(),
|
||||
events.clone(),
|
||||
mcp.clone(),
|
||||
);
|
||||
let uninstall = UninstallPlugin::new(packages.clone(), storage, registry.clone(), events, mcp);
|
||||
|
||||
install
|
||||
.execute(source.to_string_lossy().into_owned())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(app_data
|
||||
.join("plugins/installed/dev.acme.multifile/dist/constants.js")
|
||||
.is_file());
|
||||
assert!(app_data
|
||||
.join("plugins/installed/dev.acme.multifile/dist/core/util.js")
|
||||
.is_file());
|
||||
let plugin_data = app_data.join("plugins/data/dev.acme.multifile");
|
||||
fs::create_dir_all(&plugin_data).unwrap();
|
||||
fs::write(plugin_data.join("state.json"), r#"{"launches":1}"#).unwrap();
|
||||
|
||||
let uninstall_result = uninstall
|
||||
.execute(UninstallPluginInput {
|
||||
plugin_id: "dev.acme.multifile".to_owned(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
uninstall_result.removal_outcome,
|
||||
domain::RemovalOutcome::Removed
|
||||
);
|
||||
assert!(!app_data
|
||||
.join("plugins/installed/dev.acme.multifile")
|
||||
.exists());
|
||||
assert!(!app_data.join("plugins/data/dev.acme.multifile").exists());
|
||||
assert!(
|
||||
ListPlugins::new(packages.clone(), registry.clone(), validator.clone())
|
||||
.execute()
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert!(
|
||||
ListPluginRuntimeContributions::new(packages, registry, validator)
|
||||
.execute()
|
||||
.await
|
||||
.unwrap()
|
||||
.plugins
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(app_data);
|
||||
let _ = fs::remove_dir_all(source);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user