fix: harden feature contribution gates
This commit is contained in:
parent
a8ae6ca2f8
commit
40701760eb
|
|
@ -592,14 +592,82 @@ impl FeatureInstallReport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_capability(
|
||||||
|
grants: &CapabilityGrantSet,
|
||||||
|
report: &mut FeatureInstallReport,
|
||||||
|
kind: FeatureContributionKind,
|
||||||
|
name: impl Into<String>,
|
||||||
|
capability: &HostCapability,
|
||||||
|
) -> Result<(), FeatureInstallError> {
|
||||||
|
if grants.contains(capability) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let reason = format!("required capability was not granted: {capability:?}");
|
||||||
|
report.mark_skipped(kind, name, reason.clone());
|
||||||
|
Err(FeatureInstallError::CapabilityDenied(reason))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_background_task_capability(
|
||||||
|
grants: &CapabilityGrantSet,
|
||||||
|
report: &mut FeatureInstallReport,
|
||||||
|
declaration: &BackgroundTaskDeclaration,
|
||||||
|
) -> Result<(), FeatureInstallError> {
|
||||||
|
let default_capability = HostCapability::DeclareBackgroundTask {
|
||||||
|
name: declaration.name.clone(),
|
||||||
|
};
|
||||||
|
require_capability(
|
||||||
|
grants,
|
||||||
|
report,
|
||||||
|
FeatureContributionKind::BackgroundTask,
|
||||||
|
declaration.name.clone(),
|
||||||
|
&default_capability,
|
||||||
|
)?;
|
||||||
|
for capability in &declaration.required_capabilities {
|
||||||
|
require_capability(
|
||||||
|
grants,
|
||||||
|
report,
|
||||||
|
FeatureContributionKind::BackgroundTask,
|
||||||
|
declaration.name.clone(),
|
||||||
|
capability,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_service_provider_capability(
|
||||||
|
grants: &CapabilityGrantSet,
|
||||||
|
report: &mut FeatureInstallReport,
|
||||||
|
declaration: &ServiceDeclaration,
|
||||||
|
) -> Result<(), FeatureInstallError> {
|
||||||
|
let capability = HostCapability::ProvideService {
|
||||||
|
service: declaration.id.clone(),
|
||||||
|
};
|
||||||
|
require_capability(
|
||||||
|
grants,
|
||||||
|
report,
|
||||||
|
FeatureContributionKind::Service,
|
||||||
|
declaration.id.to_string(),
|
||||||
|
&capability,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Model-visible durable notification sink skeleton. The first slice exposes
|
/// Model-visible durable notification sink skeleton. The first slice exposes
|
||||||
/// the boundary without implementing a new event channel.
|
/// the boundary without implementing a new event channel.
|
||||||
pub struct FeatureNotificationSink<'a> {
|
pub struct FeatureNotificationSink<'a> {
|
||||||
|
grants: &'a CapabilityGrantSet,
|
||||||
report: &'a mut FeatureInstallReport,
|
report: &'a mut FeatureInstallReport,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeatureNotificationSink<'_> {
|
impl FeatureNotificationSink<'_> {
|
||||||
pub fn notify_model(&mut self, message: impl Into<String>) {
|
pub fn notify_model(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> {
|
||||||
|
require_capability(
|
||||||
|
self.grants,
|
||||||
|
self.report,
|
||||||
|
FeatureContributionKind::Notification,
|
||||||
|
"notify_model",
|
||||||
|
&HostCapability::EmitNotification,
|
||||||
|
)?;
|
||||||
let message = message.into();
|
let message = message.into();
|
||||||
self.report.diagnostics.push(FeatureDiagnostic::warning(format!(
|
self.report.diagnostics.push(FeatureDiagnostic::warning(format!(
|
||||||
"model notification requested during feature installation but no durable Notify host is attached: {message}"
|
"model notification requested during feature installation but no durable Notify host is attached: {message}"
|
||||||
|
|
@ -609,16 +677,25 @@ impl FeatureNotificationSink<'_> {
|
||||||
"notify_model",
|
"notify_model",
|
||||||
"durable Notify/SystemItem host is not connected during feature installation",
|
"durable Notify/SystemItem host is not connected during feature installation",
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transient human-facing alert sink skeleton.
|
/// Transient human-facing alert sink skeleton.
|
||||||
pub struct FeatureAlertSink<'a> {
|
pub struct FeatureAlertSink<'a> {
|
||||||
|
grants: &'a CapabilityGrantSet,
|
||||||
report: &'a mut FeatureInstallReport,
|
report: &'a mut FeatureInstallReport,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeatureAlertSink<'_> {
|
impl FeatureAlertSink<'_> {
|
||||||
pub fn alert(&mut self, message: impl Into<String>) {
|
pub fn alert(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> {
|
||||||
|
require_capability(
|
||||||
|
self.grants,
|
||||||
|
self.report,
|
||||||
|
FeatureContributionKind::Alert,
|
||||||
|
"alert",
|
||||||
|
&HostCapability::EmitAlert,
|
||||||
|
)?;
|
||||||
let message = message.into();
|
let message = message.into();
|
||||||
self.report
|
self.report
|
||||||
.diagnostics
|
.diagnostics
|
||||||
|
|
@ -628,29 +705,39 @@ impl FeatureAlertSink<'_> {
|
||||||
"alert",
|
"alert",
|
||||||
"transient alert host is not connected during feature installation",
|
"transient alert host is not connected during feature installation",
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Diagnostic sink available to feature installers.
|
/// Diagnostic sink available to feature installers.
|
||||||
pub struct FeatureDiagnosticSink<'a> {
|
pub struct FeatureDiagnosticSink<'a> {
|
||||||
|
grants: &'a CapabilityGrantSet,
|
||||||
report: &'a mut FeatureInstallReport,
|
report: &'a mut FeatureInstallReport,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeatureDiagnosticSink<'_> {
|
impl FeatureDiagnosticSink<'_> {
|
||||||
pub fn push(&mut self, diagnostic: FeatureDiagnostic) {
|
pub fn push(&mut self, diagnostic: FeatureDiagnostic) -> Result<(), FeatureInstallError> {
|
||||||
|
require_capability(
|
||||||
|
self.grants,
|
||||||
|
self.report,
|
||||||
|
FeatureContributionKind::Diagnostic,
|
||||||
|
"diagnostic",
|
||||||
|
&HostCapability::EmitDiagnostic,
|
||||||
|
)?;
|
||||||
self.report.diagnostics.push(diagnostic);
|
self.report.diagnostics.push(diagnostic);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn info(&mut self, message: impl Into<String>) {
|
pub fn info(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> {
|
||||||
self.push(FeatureDiagnostic::info(message));
|
self.push(FeatureDiagnostic::info(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warning(&mut self, message: impl Into<String>) {
|
pub fn warning(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> {
|
||||||
self.push(FeatureDiagnostic::warning(message));
|
self.push(FeatureDiagnostic::warning(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn error(&mut self, message: impl Into<String>) {
|
pub fn error(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> {
|
||||||
self.push(FeatureDiagnostic::error(message));
|
self.push(FeatureDiagnostic::error(message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -665,35 +752,57 @@ pub struct ToolContributionRegistrar<'a> {
|
||||||
|
|
||||||
impl ToolContributionRegistrar<'_> {
|
impl ToolContributionRegistrar<'_> {
|
||||||
pub fn register(&mut self, contribution: ToolContribution) -> Result<(), FeatureInstallError> {
|
pub fn register(&mut self, contribution: ToolContribution) -> Result<(), FeatureInstallError> {
|
||||||
for capability in &contribution.required_capabilities {
|
let model_visible_name = (contribution.definition)().0.name;
|
||||||
if !self.grants.contains(capability) {
|
if contribution.name != model_visible_name {
|
||||||
let reason = format!("required capability was not granted: {capability:?}");
|
let error = FeatureInstallError::ToolNameMismatch {
|
||||||
self.report.mark_skipped(
|
declared: contribution.name,
|
||||||
FeatureContributionKind::Tool,
|
model_visible: model_visible_name.clone(),
|
||||||
contribution.name.clone(),
|
};
|
||||||
reason.clone(),
|
self.report.mark_skipped(
|
||||||
);
|
FeatureContributionKind::Tool,
|
||||||
return Err(FeatureInstallError::CapabilityDenied(reason));
|
model_visible_name,
|
||||||
}
|
error.to_string(),
|
||||||
|
);
|
||||||
|
return Err(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(first) = self.installed_tool_names.get(&contribution.name) {
|
let tool_capability = HostCapability::ContributeTool {
|
||||||
|
name: model_visible_name.clone(),
|
||||||
|
};
|
||||||
|
require_capability(
|
||||||
|
self.grants,
|
||||||
|
self.report,
|
||||||
|
FeatureContributionKind::Tool,
|
||||||
|
model_visible_name.clone(),
|
||||||
|
&tool_capability,
|
||||||
|
)?;
|
||||||
|
for capability in &contribution.required_capabilities {
|
||||||
|
require_capability(
|
||||||
|
self.grants,
|
||||||
|
self.report,
|
||||||
|
FeatureContributionKind::Tool,
|
||||||
|
model_visible_name.clone(),
|
||||||
|
capability,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(first) = self.installed_tool_names.get(&model_visible_name) {
|
||||||
let error = FeatureInstallError::DuplicateToolName {
|
let error = FeatureInstallError::DuplicateToolName {
|
||||||
tool: contribution.name.clone(),
|
tool: model_visible_name.clone(),
|
||||||
first_feature: first.to_string(),
|
first_feature: first.to_string(),
|
||||||
duplicate_feature: self.feature_id.to_string(),
|
duplicate_feature: self.feature_id.to_string(),
|
||||||
};
|
};
|
||||||
self.report.mark_skipped(
|
self.report.mark_skipped(
|
||||||
FeatureContributionKind::Tool,
|
FeatureContributionKind::Tool,
|
||||||
contribution.name.clone(),
|
model_visible_name,
|
||||||
error.to_string(),
|
error.to_string(),
|
||||||
);
|
);
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.installed_tool_names
|
self.installed_tool_names
|
||||||
.insert(contribution.name.clone(), self.feature_id.clone());
|
.insert(model_visible_name.clone(), self.feature_id.clone());
|
||||||
self.report.installed_tools.push(contribution.name);
|
self.report.installed_tools.push(model_visible_name);
|
||||||
self.pending_tools.push(contribution.definition);
|
self.pending_tools.push(contribution.definition);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -776,24 +885,32 @@ impl HookContributionRegistrar<'_> {
|
||||||
|
|
||||||
/// Background task registrar for descriptor/report-only contributions.
|
/// Background task registrar for descriptor/report-only contributions.
|
||||||
pub struct BackgroundTaskRegistrar<'a> {
|
pub struct BackgroundTaskRegistrar<'a> {
|
||||||
|
grants: &'a CapabilityGrantSet,
|
||||||
report: &'a mut FeatureInstallReport,
|
report: &'a mut FeatureInstallReport,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackgroundTaskRegistrar<'_> {
|
impl BackgroundTaskRegistrar<'_> {
|
||||||
pub fn declare(&mut self, declaration: BackgroundTaskDeclaration) {
|
pub fn declare(
|
||||||
|
&mut self,
|
||||||
|
declaration: BackgroundTaskDeclaration,
|
||||||
|
) -> Result<(), FeatureInstallError> {
|
||||||
|
require_background_task_capability(self.grants, self.report, &declaration)?;
|
||||||
self.report.declared_background_tasks.push(declaration);
|
self.report.declared_background_tasks.push(declaration);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Service registrar for descriptor/report-only provider metadata.
|
/// Service registrar for descriptor/report-only provider metadata.
|
||||||
pub struct FeatureServiceRegistrar<'a> {
|
pub struct FeatureServiceRegistrar<'a> {
|
||||||
feature_id: &'a FeatureId,
|
feature_id: &'a FeatureId,
|
||||||
|
grants: &'a CapabilityGrantSet,
|
||||||
service_registry: &'a mut FeatureServiceRegistry,
|
service_registry: &'a mut FeatureServiceRegistry,
|
||||||
report: &'a mut FeatureInstallReport,
|
report: &'a mut FeatureInstallReport,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeatureServiceRegistrar<'_> {
|
impl FeatureServiceRegistrar<'_> {
|
||||||
pub fn provide(&mut self, declaration: ServiceDeclaration) -> Result<(), FeatureInstallError> {
|
pub fn provide(&mut self, declaration: ServiceDeclaration) -> Result<(), FeatureInstallError> {
|
||||||
|
require_service_provider_capability(self.grants, self.report, &declaration)?;
|
||||||
self.service_registry
|
self.service_registry
|
||||||
.register_provider(self.feature_id.clone(), declaration.clone())?;
|
.register_provider(self.feature_id.clone(), declaration.clone())?;
|
||||||
self.report.provided_services.push(declaration);
|
self.report.provided_services.push(declaration);
|
||||||
|
|
@ -841,6 +958,7 @@ impl FeatureInstallContext<'_> {
|
||||||
|
|
||||||
pub fn background_tasks(&mut self) -> BackgroundTaskRegistrar<'_> {
|
pub fn background_tasks(&mut self) -> BackgroundTaskRegistrar<'_> {
|
||||||
BackgroundTaskRegistrar {
|
BackgroundTaskRegistrar {
|
||||||
|
grants: self.grants,
|
||||||
report: self.report,
|
report: self.report,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -848,6 +966,7 @@ impl FeatureInstallContext<'_> {
|
||||||
pub fn services(&mut self) -> FeatureServiceRegistrar<'_> {
|
pub fn services(&mut self) -> FeatureServiceRegistrar<'_> {
|
||||||
FeatureServiceRegistrar {
|
FeatureServiceRegistrar {
|
||||||
feature_id: self.feature_id,
|
feature_id: self.feature_id,
|
||||||
|
grants: self.grants,
|
||||||
service_registry: self.service_registry,
|
service_registry: self.service_registry,
|
||||||
report: self.report,
|
report: self.report,
|
||||||
}
|
}
|
||||||
|
|
@ -855,18 +974,21 @@ impl FeatureInstallContext<'_> {
|
||||||
|
|
||||||
pub fn notifications(&mut self) -> FeatureNotificationSink<'_> {
|
pub fn notifications(&mut self) -> FeatureNotificationSink<'_> {
|
||||||
FeatureNotificationSink {
|
FeatureNotificationSink {
|
||||||
|
grants: self.grants,
|
||||||
report: self.report,
|
report: self.report,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn alerts(&mut self) -> FeatureAlertSink<'_> {
|
pub fn alerts(&mut self) -> FeatureAlertSink<'_> {
|
||||||
FeatureAlertSink {
|
FeatureAlertSink {
|
||||||
|
grants: self.grants,
|
||||||
report: self.report,
|
report: self.report,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn diagnostics(&mut self) -> FeatureDiagnosticSink<'_> {
|
pub fn diagnostics(&mut self) -> FeatureDiagnosticSink<'_> {
|
||||||
FeatureDiagnosticSink {
|
FeatureDiagnosticSink {
|
||||||
|
grants: self.grants,
|
||||||
report: self.report,
|
report: self.report,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -978,24 +1100,47 @@ impl FeatureRegistryBuilder {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(reason) = missing_required_service(&descriptor, &service_registry) {
|
let mut required_service_failed = false;
|
||||||
report
|
|
||||||
.diagnostics
|
|
||||||
.push(FeatureDiagnostic::error(reason.clone()));
|
|
||||||
report.mark_skipped(
|
|
||||||
FeatureContributionKind::Service,
|
|
||||||
descriptor.id.to_string(),
|
|
||||||
reason,
|
|
||||||
);
|
|
||||||
reports.push(report);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for requirement in descriptor.requires_services.iter().cloned() {
|
for requirement in descriptor.requires_services.iter().cloned() {
|
||||||
|
let capability = HostCapability::RequireService {
|
||||||
|
service: requirement.id.clone(),
|
||||||
|
};
|
||||||
|
if let Err(error) = require_capability(
|
||||||
|
&grants,
|
||||||
|
&mut report,
|
||||||
|
FeatureContributionKind::Service,
|
||||||
|
requirement.id.to_string(),
|
||||||
|
&capability,
|
||||||
|
) {
|
||||||
|
if requirement.required {
|
||||||
|
report
|
||||||
|
.diagnostics
|
||||||
|
.push(FeatureDiagnostic::error(error.to_string()));
|
||||||
|
required_service_failed = true;
|
||||||
|
} else {
|
||||||
|
report
|
||||||
|
.diagnostics
|
||||||
|
.push(FeatureDiagnostic::warning(error.to_string()));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if service_registry.provides(&requirement.id) {
|
if service_registry.provides(&requirement.id) {
|
||||||
report.resolved_service_requirements.push(requirement);
|
report.resolved_service_requirements.push(requirement);
|
||||||
} else if requirement.required {
|
} else if requirement.required {
|
||||||
// Already handled by missing_required_service.
|
let reason = format!(
|
||||||
|
"required service requirement is not available: {}",
|
||||||
|
requirement.id
|
||||||
|
);
|
||||||
|
report
|
||||||
|
.diagnostics
|
||||||
|
.push(FeatureDiagnostic::error(reason.clone()));
|
||||||
|
report.mark_skipped(
|
||||||
|
FeatureContributionKind::Service,
|
||||||
|
requirement.id.to_string(),
|
||||||
|
reason,
|
||||||
|
);
|
||||||
|
required_service_failed = true;
|
||||||
} else {
|
} else {
|
||||||
report.diagnostics.push(FeatureDiagnostic::warning(format!(
|
report.diagnostics.push(FeatureDiagnostic::warning(format!(
|
||||||
"optional service requirement is not available: {}",
|
"optional service requirement is not available: {}",
|
||||||
|
|
@ -1008,24 +1153,40 @@ impl FeatureRegistryBuilder {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if required_service_failed {
|
||||||
|
reports.push(report);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for background_task in descriptor.background_tasks.iter().cloned() {
|
for background_task in descriptor.background_tasks.iter().cloned() {
|
||||||
report.declared_background_tasks.push(background_task);
|
match require_background_task_capability(&grants, &mut report, &background_task) {
|
||||||
|
Ok(()) => report.declared_background_tasks.push(background_task),
|
||||||
|
Err(error) => report
|
||||||
|
.diagnostics
|
||||||
|
.push(FeatureDiagnostic::warning(error.to_string())),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for service in descriptor.provides_services.iter().cloned() {
|
for service in descriptor.provides_services.iter().cloned() {
|
||||||
match service_registry.register_provider(descriptor.id.clone(), service.clone()) {
|
match require_service_provider_capability(&grants, &mut report, &service) {
|
||||||
Ok(()) => report.provided_services.push(service),
|
Ok(()) => match service_registry
|
||||||
Err(error) => {
|
.register_provider(descriptor.id.clone(), service.clone())
|
||||||
report
|
{
|
||||||
.diagnostics
|
Ok(()) => report.provided_services.push(service),
|
||||||
.push(FeatureDiagnostic::error(error.to_string()));
|
Err(error) => {
|
||||||
report.mark_skipped(
|
report
|
||||||
FeatureContributionKind::Service,
|
.diagnostics
|
||||||
service.id.to_string(),
|
.push(FeatureDiagnostic::error(error.to_string()));
|
||||||
error.to_string(),
|
report.mark_skipped(
|
||||||
);
|
FeatureContributionKind::Service,
|
||||||
}
|
service.id.to_string(),
|
||||||
|
error.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => report
|
||||||
|
.diagnostics
|
||||||
|
.push(FeatureDiagnostic::warning(error.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1060,22 +1221,6 @@ impl FeatureRegistryBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn missing_required_service(
|
|
||||||
descriptor: &FeatureDescriptor,
|
|
||||||
services: &FeatureServiceRegistry,
|
|
||||||
) -> Option<String> {
|
|
||||||
descriptor
|
|
||||||
.requires_services
|
|
||||||
.iter()
|
|
||||||
.find(|requirement| requirement.required && !services.provides(&requirement.id))
|
|
||||||
.map(|requirement| {
|
|
||||||
format!(
|
|
||||||
"required service requirement is not available: {}",
|
|
||||||
requirement.id
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Feature installation errors.
|
/// Feature installation errors.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum FeatureInstallError {
|
pub enum FeatureInstallError {
|
||||||
|
|
@ -1089,6 +1234,13 @@ pub enum FeatureInstallError {
|
||||||
first_feature: String,
|
first_feature: String,
|
||||||
duplicate_feature: String,
|
duplicate_feature: String,
|
||||||
},
|
},
|
||||||
|
#[error(
|
||||||
|
"tool contribution declared name `{declared}` does not match model-visible tool name `{model_visible}`"
|
||||||
|
)]
|
||||||
|
ToolNameMismatch {
|
||||||
|
declared: String,
|
||||||
|
model_visible: String,
|
||||||
|
},
|
||||||
#[error(
|
#[error(
|
||||||
"duplicate service declaration `{service}` from feature `{duplicate_feature}`; first provided by `{first_feature}`"
|
"duplicate service declaration `{service}` from feature `{duplicate_feature}`; first provided by `{first_feature}`"
|
||||||
)]
|
)]
|
||||||
|
|
@ -1223,7 +1375,8 @@ mod tests {
|
||||||
|
|
||||||
struct ToolFeature {
|
struct ToolFeature {
|
||||||
descriptor: FeatureDescriptor,
|
descriptor: FeatureDescriptor,
|
||||||
tool_name: &'static str,
|
contribution_name: &'static str,
|
||||||
|
model_visible_name: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeatureModule for ToolFeature {
|
impl FeatureModule for ToolFeature {
|
||||||
|
|
@ -1236,8 +1389,8 @@ mod tests {
|
||||||
context: &mut FeatureInstallContext<'_>,
|
context: &mut FeatureInstallContext<'_>,
|
||||||
) -> Result<(), FeatureInstallError> {
|
) -> Result<(), FeatureInstallError> {
|
||||||
context.tools().register(ToolContribution::new(
|
context.tools().register(ToolContribution::new(
|
||||||
self.tool_name,
|
self.contribution_name,
|
||||||
dummy_tool(self.tool_name),
|
dummy_tool(self.model_visible_name),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1251,6 +1404,12 @@ mod tests {
|
||||||
},
|
},
|
||||||
"test",
|
"test",
|
||||||
))
|
))
|
||||||
|
.with_capability(CapabilityRequest::required(
|
||||||
|
HostCapability::DeclareBackgroundTask {
|
||||||
|
name: "daily".into(),
|
||||||
|
},
|
||||||
|
"test background task declaration",
|
||||||
|
))
|
||||||
.with_tool(ToolDeclaration::new("Dummy", "dummy tool"))
|
.with_tool(ToolDeclaration::new("Dummy", "dummy tool"))
|
||||||
.with_background_task(BackgroundTaskDeclaration::descriptor_only(
|
.with_background_task(BackgroundTaskDeclaration::descriptor_only(
|
||||||
"daily",
|
"daily",
|
||||||
|
|
@ -1261,7 +1420,8 @@ mod tests {
|
||||||
let report = FeatureRegistryBuilder::new()
|
let report = FeatureRegistryBuilder::new()
|
||||||
.with_module(ToolFeature {
|
.with_module(ToolFeature {
|
||||||
descriptor,
|
descriptor,
|
||||||
tool_name: "Dummy",
|
contribution_name: "Dummy",
|
||||||
|
model_visible_name: "Dummy",
|
||||||
})
|
})
|
||||||
.install_into_pending(&mut pending_tools, &mut hook_builder);
|
.install_into_pending(&mut pending_tools, &mut hook_builder);
|
||||||
|
|
||||||
|
|
@ -1303,11 +1463,13 @@ mod tests {
|
||||||
let report = FeatureRegistryBuilder::new()
|
let report = FeatureRegistryBuilder::new()
|
||||||
.with_module(ToolFeature {
|
.with_module(ToolFeature {
|
||||||
descriptor: descriptor_a,
|
descriptor: descriptor_a,
|
||||||
tool_name: "Duplicate",
|
contribution_name: "Duplicate",
|
||||||
|
model_visible_name: "Duplicate",
|
||||||
})
|
})
|
||||||
.with_module(ToolFeature {
|
.with_module(ToolFeature {
|
||||||
descriptor: descriptor_b,
|
descriptor: descriptor_b,
|
||||||
tool_name: "Duplicate",
|
contribution_name: "Duplicate",
|
||||||
|
model_visible_name: "Duplicate",
|
||||||
})
|
})
|
||||||
.install_into_pending(&mut pending_tools, &mut hook_builder);
|
.install_into_pending(&mut pending_tools, &mut hook_builder);
|
||||||
|
|
||||||
|
|
@ -1326,6 +1488,36 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mismatched_tool_contribution_name_is_rejected_before_queueing() {
|
||||||
|
let descriptor = FeatureDescriptor::builtin("mismatch", "Mismatch")
|
||||||
|
.with_capability(CapabilityRequest::required(
|
||||||
|
HostCapability::ContributeTool {
|
||||||
|
name: "Actual".into(),
|
||||||
|
},
|
||||||
|
"test mismatch handling",
|
||||||
|
))
|
||||||
|
.with_tool(ToolDeclaration::new("Actual", "actual model-visible tool"));
|
||||||
|
let mut hook_builder = HookRegistryBuilder::default();
|
||||||
|
let mut pending_tools = Vec::new();
|
||||||
|
let report = FeatureRegistryBuilder::new()
|
||||||
|
.with_module(ToolFeature {
|
||||||
|
descriptor,
|
||||||
|
contribution_name: "Declared",
|
||||||
|
model_visible_name: "Actual",
|
||||||
|
})
|
||||||
|
.install_into_pending(&mut pending_tools, &mut hook_builder);
|
||||||
|
|
||||||
|
assert!(pending_tools.is_empty());
|
||||||
|
assert!(!report.reports[0].installed);
|
||||||
|
assert!(report.reports[0].diagnostics.iter().any(|diagnostic| {
|
||||||
|
diagnostic
|
||||||
|
.message
|
||||||
|
.contains("does not match model-visible tool name")
|
||||||
|
}));
|
||||||
|
assert_eq!(report.reports[0].skipped[0].name, "Actual");
|
||||||
|
}
|
||||||
|
|
||||||
struct ServiceFeature {
|
struct ServiceFeature {
|
||||||
descriptor: FeatureDescriptor,
|
descriptor: FeatureDescriptor,
|
||||||
}
|
}
|
||||||
|
|
@ -1346,17 +1538,50 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn service_requirements_resolve_against_prior_providers() {
|
fn service_requirements_resolve_against_prior_providers() {
|
||||||
let service = ServiceId::builtin("demo-service");
|
let service = ServiceId::builtin("demo-service");
|
||||||
let provider = FeatureDescriptor::builtin("provider", "Provider").with_provided_service(
|
let provider = FeatureDescriptor::builtin("provider", "Provider")
|
||||||
ServiceDeclaration::new(service.clone(), "1", "demo service"),
|
.with_capability(CapabilityRequest::required(
|
||||||
);
|
HostCapability::ProvideService {
|
||||||
|
service: service.clone(),
|
||||||
|
},
|
||||||
|
"provide demo service",
|
||||||
|
))
|
||||||
|
.with_provided_service(ServiceDeclaration::new(
|
||||||
|
service.clone(),
|
||||||
|
"1",
|
||||||
|
"demo service",
|
||||||
|
));
|
||||||
let consumer = FeatureDescriptor::builtin("consumer", "Consumer")
|
let consumer = FeatureDescriptor::builtin("consumer", "Consumer")
|
||||||
|
.with_capability(CapabilityRequest::required(
|
||||||
|
HostCapability::RequireService {
|
||||||
|
service: service.clone(),
|
||||||
|
},
|
||||||
|
"require demo service",
|
||||||
|
))
|
||||||
.with_service_requirement(ServiceRequirement::required(service.clone(), "needs demo"));
|
.with_service_requirement(ServiceRequirement::required(service.clone(), "needs demo"));
|
||||||
let missing = FeatureDescriptor::builtin("missing", "Missing").with_service_requirement(
|
let missing_service = ServiceId::builtin("missing-service");
|
||||||
ServiceRequirement::required(ServiceId::builtin("missing-service"), "needs missing"),
|
let missing = FeatureDescriptor::builtin("missing", "Missing")
|
||||||
);
|
.with_capability(CapabilityRequest::required(
|
||||||
let optional = FeatureDescriptor::builtin("optional", "Optional").with_service_requirement(
|
HostCapability::RequireService {
|
||||||
ServiceRequirement::optional(ServiceId::builtin("optional-service"), "nice to have"),
|
service: missing_service.clone(),
|
||||||
);
|
},
|
||||||
|
"require missing service",
|
||||||
|
))
|
||||||
|
.with_service_requirement(ServiceRequirement::required(
|
||||||
|
missing_service,
|
||||||
|
"needs missing",
|
||||||
|
));
|
||||||
|
let optional_service = ServiceId::builtin("optional-service");
|
||||||
|
let optional = FeatureDescriptor::builtin("optional", "Optional")
|
||||||
|
.with_capability(CapabilityRequest::required(
|
||||||
|
HostCapability::RequireService {
|
||||||
|
service: optional_service.clone(),
|
||||||
|
},
|
||||||
|
"optionally require service",
|
||||||
|
))
|
||||||
|
.with_service_requirement(ServiceRequirement::optional(
|
||||||
|
optional_service,
|
||||||
|
"nice to have",
|
||||||
|
));
|
||||||
let mut hook_builder = HookRegistryBuilder::default();
|
let mut hook_builder = HookRegistryBuilder::default();
|
||||||
let mut pending_tools = Vec::new();
|
let mut pending_tools = Vec::new();
|
||||||
let report = FeatureRegistryBuilder::new()
|
let report = FeatureRegistryBuilder::new()
|
||||||
|
|
@ -1394,6 +1619,59 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn background_task_declaration_without_capability_is_skipped() {
|
||||||
|
let descriptor = FeatureDescriptor::builtin("background-denied", "Background denied")
|
||||||
|
.with_background_task(BackgroundTaskDeclaration::descriptor_only(
|
||||||
|
"denied-task",
|
||||||
|
"should be skipped",
|
||||||
|
));
|
||||||
|
let mut hook_builder = HookRegistryBuilder::default();
|
||||||
|
let mut pending_tools = Vec::new();
|
||||||
|
let report = FeatureRegistryBuilder::new()
|
||||||
|
.with_module(ServiceFeature { descriptor })
|
||||||
|
.install_into_pending(&mut pending_tools, &mut hook_builder);
|
||||||
|
|
||||||
|
assert!(report.reports[0].installed);
|
||||||
|
assert!(report.reports[0].declared_background_tasks.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
report.reports[0].skipped[0].kind,
|
||||||
|
FeatureContributionKind::BackgroundTask
|
||||||
|
);
|
||||||
|
assert!(report.reports[0].diagnostics.iter().any(|diagnostic| {
|
||||||
|
diagnostic
|
||||||
|
.message
|
||||||
|
.contains("required capability was not granted")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn service_provider_without_capability_is_skipped() {
|
||||||
|
let service = ServiceId::builtin("denied-service");
|
||||||
|
let descriptor =
|
||||||
|
FeatureDescriptor::builtin("service-denied", "Service denied").with_provided_service(
|
||||||
|
ServiceDeclaration::new(service.clone(), "1", "should be skipped"),
|
||||||
|
);
|
||||||
|
let mut hook_builder = HookRegistryBuilder::default();
|
||||||
|
let mut pending_tools = Vec::new();
|
||||||
|
let report = FeatureRegistryBuilder::new()
|
||||||
|
.with_module(ServiceFeature { descriptor })
|
||||||
|
.install_into_pending(&mut pending_tools, &mut hook_builder);
|
||||||
|
|
||||||
|
assert!(report.reports[0].installed);
|
||||||
|
assert!(!report.services.provides(&service));
|
||||||
|
assert!(report.reports[0].provided_services.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
report.reports[0].skipped[0].kind,
|
||||||
|
FeatureContributionKind::Service
|
||||||
|
);
|
||||||
|
assert!(report.reports[0].diagnostics.iter().any(|diagnostic| {
|
||||||
|
diagnostic
|
||||||
|
.message
|
||||||
|
.contains("required capability was not granted")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_task_feature_installs_through_worker_tool_path() {
|
fn builtin_task_feature_installs_through_worker_tool_path() {
|
||||||
let task_store = tools::TaskStore::new();
|
let task_store = tools::TaskStore::new();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user