fix(ticket116): isoler crash plugin hello-plugin + erreur explicite UI
This commit is contained in:
@ -713,44 +713,99 @@ impl ListPluginRuntimeContributions {
|
||||
|
||||
/// Executes the use case.
|
||||
pub async fn execute(&self) -> Result<PluginRuntimeCatalog, AppError> {
|
||||
let registry = self.registry.load_registry().await.map_err(map_registry)?;
|
||||
let mut registry = self.registry.load_registry().await.map_err(map_registry)?;
|
||||
let mut plugins = Vec::new();
|
||||
for entry in registry.plugins {
|
||||
let mut invalid_plugins = Vec::new();
|
||||
for entry in registry.plugins.clone() {
|
||||
if !entry.lifecycle_state.is_runtime_active() {
|
||||
continue;
|
||||
}
|
||||
let descriptor =
|
||||
descriptor_for(self.packages.as_ref(), self.validator.as_ref(), entry).await?;
|
||||
let bundle = plugin_asset_url(
|
||||
&descriptor.manifest.id,
|
||||
descriptor.manifest.version.as_str(),
|
||||
&descriptor.registry.content_hash,
|
||||
&descriptor.manifest.main,
|
||||
);
|
||||
let icon_url = match &descriptor.manifest.icon {
|
||||
Some(icon) => Some(plugin_asset_url(
|
||||
&descriptor.manifest.id,
|
||||
descriptor.manifest.version.as_str(),
|
||||
&descriptor.registry.content_hash,
|
||||
icon,
|
||||
)),
|
||||
None => None,
|
||||
};
|
||||
plugins.push(PluginRuntimePlugin {
|
||||
id: descriptor.manifest.id.as_str().to_owned(),
|
||||
display_name: descriptor.manifest.display_name,
|
||||
publisher: descriptor.manifest.publisher,
|
||||
version: descriptor.manifest.version.as_str().to_owned(),
|
||||
bundle_url: bundle,
|
||||
icon_url,
|
||||
content_hash: descriptor.registry.content_hash.as_str().to_owned(),
|
||||
contributes: descriptor.manifest.contributes,
|
||||
});
|
||||
match runtime_plugin_from_entry(
|
||||
self.packages.as_ref(),
|
||||
self.validator.as_ref(),
|
||||
entry.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(plugin) => plugins.push(plugin),
|
||||
Err(err) => {
|
||||
let message = format!(
|
||||
"runtime contributions disabled: plugin `{}` is not servable: {err}",
|
||||
entry.id.as_str()
|
||||
);
|
||||
crate::diag!("[plugins] {message}");
|
||||
invalid_plugins.push((entry.id, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !invalid_plugins.is_empty() {
|
||||
for (plugin_id, message) in invalid_plugins {
|
||||
if let Some(entry) = registry.plugins.iter_mut().find(|p| p.id == plugin_id) {
|
||||
entry.lifecycle_state = PluginLifecycleState::Invalid;
|
||||
entry.error = Some(message);
|
||||
}
|
||||
}
|
||||
if let Err(err) = self
|
||||
.registry
|
||||
.save_registry(®istry)
|
||||
.await
|
||||
.map_err(map_registry)
|
||||
{
|
||||
crate::diag!("[plugins] failed to persist invalid runtime plugin state: {err}");
|
||||
}
|
||||
}
|
||||
Ok(PluginRuntimeCatalog { plugins })
|
||||
}
|
||||
}
|
||||
|
||||
async fn runtime_plugin_from_entry(
|
||||
packages: &dyn PluginPackageStore,
|
||||
validator: &dyn PluginManifestValidator,
|
||||
entry: PluginRegistryEntry,
|
||||
) -> Result<PluginRuntimePlugin, AppError> {
|
||||
let descriptor = descriptor_for(packages, validator, entry).await?;
|
||||
let bundle = checked_plugin_asset_url(
|
||||
packages,
|
||||
&descriptor.manifest.id,
|
||||
descriptor.manifest.version.as_str(),
|
||||
&descriptor.registry.content_hash,
|
||||
&descriptor.manifest.main,
|
||||
)?;
|
||||
let icon_url = match &descriptor.manifest.icon {
|
||||
Some(icon) => Some(checked_plugin_asset_url(
|
||||
packages,
|
||||
&descriptor.manifest.id,
|
||||
descriptor.manifest.version.as_str(),
|
||||
&descriptor.registry.content_hash,
|
||||
icon,
|
||||
)?),
|
||||
None => None,
|
||||
};
|
||||
Ok(PluginRuntimePlugin {
|
||||
id: descriptor.manifest.id.as_str().to_owned(),
|
||||
display_name: descriptor.manifest.display_name,
|
||||
publisher: descriptor.manifest.publisher,
|
||||
version: descriptor.manifest.version.as_str().to_owned(),
|
||||
bundle_url: bundle,
|
||||
icon_url,
|
||||
content_hash: descriptor.registry.content_hash.as_str().to_owned(),
|
||||
contributes: descriptor.manifest.contributes,
|
||||
})
|
||||
}
|
||||
|
||||
fn checked_plugin_asset_url(
|
||||
packages: &dyn PluginPackageStore,
|
||||
plugin_id: &PluginId,
|
||||
version: &str,
|
||||
hash: &ContentHash,
|
||||
path: &domain::RelativePath,
|
||||
) -> Result<String, AppError> {
|
||||
packages
|
||||
.bundle_url(plugin_id, path, hash)
|
||||
.map_err(map_store)?;
|
||||
Ok(plugin_asset_url(plugin_id, version, hash, path))
|
||||
}
|
||||
|
||||
/// Reconciles plugin MCP servers.
|
||||
pub struct ReconcilePluginMcpServers {
|
||||
packages: Arc<dyn PluginPackageStore>,
|
||||
@ -1541,6 +1596,34 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_catalog_marks_invalid_active_plugin_and_keeps_bootstrap_alive() {
|
||||
let packages = Arc::new(FakePackages::with_manifest(br#"{"broken":true}"#.to_vec()));
|
||||
let registry = Arc::new(FakeRegistry {
|
||||
registry: Mutex::new(registry_with(PluginLifecycleState::Enabled)),
|
||||
});
|
||||
let usecase =
|
||||
ListPluginRuntimeContributions::new(packages, registry.clone(), Arc::new(validator()));
|
||||
|
||||
let catalog = usecase.execute().await.unwrap();
|
||||
|
||||
assert!(
|
||||
catalog.plugins.is_empty(),
|
||||
"invalid active plugin must be excluded from runtime catalog"
|
||||
);
|
||||
let saved = registry.load_registry().await.unwrap();
|
||||
let entry = saved.find(&plugin_id()).unwrap();
|
||||
assert_eq!(entry.lifecycle_state, PluginLifecycleState::Invalid);
|
||||
assert!(
|
||||
entry
|
||||
.error
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.contains("not servable"),
|
||||
"registry must carry a confined runtime error: {entry:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconcile_mcp_uses_only_enabled_auto_start_servers_with_plugin_identity() {
|
||||
let packages = Arc::new(FakePackages::with_manifest(valid_manifest()));
|
||||
|
||||
Reference in New Issue
Block a user