refactor: remove ambient plugin authority

This commit is contained in:
2026-09-14 18:40:42 +09:00
parent d2cb50d081
commit a61ad15767
21 changed files with 414 additions and 14736 deletions
+15 -26
View File
@@ -15,7 +15,6 @@ use serde::{Deserialize, Serialize};
use crate::defaults;
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
use crate::plugin::PluginConfig;
use crate::{
CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits,
McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConsolidationProfileConfig,
@@ -55,10 +54,6 @@ pub struct WorkerManifestConfig {
/// disabled after cascade merge.
#[serde(default)]
pub feature: FeatureConfigPartial,
/// Explicit plugin package enablement entries. Discovery/resolution is a
/// separate step and does not run during config merge.
#[serde(default)]
pub plugins: PluginConfig,
/// Explicit Model Context Protocol provider declarations. Config parsing
/// never starts a local MCP subprocess.
#[serde(default)]
@@ -74,6 +69,7 @@ pub struct WorkerManifestConfig {
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeatureConfigPartial {
#[serde(default)]
pub task: Option<FeatureFlagConfigPartial>,
@@ -101,8 +97,6 @@ pub struct FeatureConfigPartial {
pub merge_request: Option<MergeRequestFeatureConfigPartial>,
#[serde(default)]
pub orchestration: Option<FeatureFlagConfigPartial>,
#[serde(default)]
pub plugins: Option<FeatureFlagConfigPartial>,
}
impl FeatureConfigPartial {
@@ -145,7 +139,6 @@ impl FeatureConfigPartial {
other.orchestration,
FeatureFlagConfigPartial::merge,
),
plugins: merge_option(self.plugins, other.plugins, FeatureFlagConfigPartial::merge),
}
}
}
@@ -370,10 +363,6 @@ impl From<FeatureConfigPartial> for FeatureConfig {
.orchestration
.map(FeatureFlagConfig::from)
.unwrap_or_default(),
plugins: value
.plugins
.map(FeatureFlagConfig::from)
.unwrap_or_default(),
}
}
}
@@ -517,7 +506,6 @@ impl From<FeatureConfig> for FeatureConfigPartial {
ticket: Some(value.ticket.into()),
merge_request: Some(value.merge_request.into()),
orchestration: Some(value.orchestration.into()),
plugins: Some(value.plugins.into()),
}
}
}
@@ -654,6 +642,20 @@ pub(crate) fn reject_removed_manifest_fields(s: &str) -> Result<(), toml::de::Er
"unknown field in manifest: memory (removed; configure feature.memory)",
));
}
if value.get("plugins").is_some() {
return Err(toml::de::Error::custom(
"unknown field in manifest: plugins (dynamic Plugins are not supported)",
));
}
if value
.get("feature")
.and_then(toml::Value::as_table)
.is_some_and(|table| table.contains_key("plugins"))
{
return Err(toml::de::Error::custom(
"unknown field in manifest: feature.plugins (dynamic Plugins are not supported)",
));
}
if value
.get("feature")
.and_then(toml::Value::as_table)
@@ -771,7 +773,6 @@ impl WorkerManifestConfig {
PermissionConfigPartial::merge,
),
feature: self.feature.merge(upper.feature),
plugins: merge_plugin_config(self.plugins, upper.plugins),
mcp: merge_mcp_config(self.mcp, upper.mcp),
compaction: merge_option(
self.compaction,
@@ -791,16 +792,6 @@ impl SkillsConfig {
}
}
fn merge_plugin_config(mut base: PluginConfig, upper: PluginConfig) -> PluginConfig {
let upper_has_resolved_plan = upper.has_resolved_plan();
base.enabled.extend(upper.enabled);
if upper_has_resolved_plan {
base.resolved = upper.resolved;
base.diagnostics = upper.diagnostics;
}
base
}
fn merge_mcp_config(mut base: McpConfig, upper: McpConfig) -> McpConfig {
base.stdio_servers.extend(upper.stdio_servers);
base
@@ -1289,7 +1280,6 @@ impl TryFrom<WorkerManifestConfig> for WorkerManifest {
session,
permissions,
feature: FeatureConfig::from(cfg.feature),
plugins: cfg.plugins,
mcp: cfg.mcp,
compaction,
web: cfg.web,
@@ -1335,7 +1325,6 @@ mod tests {
delegation_scope: ScopeConfig::default(),
permissions: None,
feature: FeatureConfigPartial::default(),
plugins: PluginConfig::default(),
mcp: McpConfig::default(),
session: None,
compaction: None,
+52 -32
View File
@@ -69,10 +69,6 @@ pub struct WorkerManifest {
/// resolve disabled so Profile authors choose the exposed built-in surfaces.
#[serde(default)]
pub feature: FeatureConfig,
/// Explicit plugin package enablement. Discovery remains read-only; only
/// source-qualified entries listed here may resolve to active plugin metadata.
#[serde(default)]
pub plugins: plugin::PluginConfig,
/// Explicit external Model Context Protocol provider configuration. This
/// is config data only: declaring a server never starts a subprocess or
/// grants OS sandboxing. Runtime MCP lifecycle/registration is a separate
@@ -106,6 +102,7 @@ pub struct WorkerManifest {
/// sessions, secrets, or resolved host state. Tool registration still applies
/// the normal scope, host-authority, backend, memory, and network checks.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct FeatureConfig {
#[serde(default)]
pub task: FeatureFlagConfig,
@@ -135,8 +132,6 @@ pub struct FeatureConfig {
pub merge_request: MergeRequestFeatureConfig,
#[serde(default)]
pub orchestration: FeatureFlagConfig,
#[serde(default)]
pub plugins: FeatureFlagConfig,
}
impl Default for FeatureConfig {
@@ -155,7 +150,6 @@ impl Default for FeatureConfig {
ticket: TicketFeatureConfig::default(),
merge_request: MergeRequestFeatureConfig::default(),
orchestration: FeatureFlagConfig::disabled(),
plugins: FeatureFlagConfig::disabled(),
}
}
}
@@ -941,9 +935,7 @@ impl Default for CompactionConfig {
impl WorkerManifest {
pub fn requires_persisted_execution_snapshot(&self) -> bool {
self.profile.is_some()
|| self.plugins.has_resolved_plan()
|| self.feature.memory.workspace_settings.is_some()
self.profile.is_some() || self.feature.memory.workspace_settings.is_some()
}
/// Parse a manifest from a TOML string.
@@ -1322,33 +1314,61 @@ model_id = "claude-sonnet-4-20250514"
}
#[test]
fn parse_plugin_enablement_config() {
fn dynamic_plugin_manifest_config_is_rejected() {
let toml = format!(
"{MINIMAL_REQUIRED}\n\
[[plugins.enabled]]\n\
id = \"project:example\"\n\
version = \"0.1.0\"\n\
digest = \"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n\
surfaces = [\"hook\"]\n\n\
[plugins.enabled.config]\n\
greeting = \"hello\"\n"
id = \"project:example\"\n"
);
let manifest = WorkerManifest::from_toml(&toml).unwrap();
assert_eq!(manifest.plugins.enabled.len(), 1);
let enabled = &manifest.plugins.enabled[0];
assert_eq!(enabled.id, "project:example");
assert_eq!(
enabled.version.as_ref().map(|version| version.0.as_str()),
Some("0.1.0")
let error = WorkerManifest::from_toml(&toml).unwrap_err();
assert!(
error
.to_string()
.contains("dynamic Plugins are not supported"),
"unexpected error: {error}"
);
assert_eq!(enabled.surfaces, vec![plugin::PluginSurface::Hook]);
assert_eq!(
enabled
.config
.as_ref()
.and_then(|value| value.get("greeting"))
.and_then(|value| value.as_str()),
Some("hello")
}
#[test]
fn persisted_manifest_with_dynamic_plugin_plan_is_rejected() {
let base =
serde_json::to_value(WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap()).unwrap();
let mut top_level = base.clone();
top_level.as_object_mut().unwrap().insert(
"plugins".to_string(),
serde_json::json!({
"resolved": [{
"package_path": "/tmp/ambient.yoi-plugin"
}]
}),
);
let error = serde_json::from_value::<WorkerManifest>(top_level).unwrap_err();
assert!(error.to_string().contains("unknown field `plugins`"));
let mut nested = base;
nested
.get_mut("feature")
.unwrap()
.as_object_mut()
.unwrap()
.insert(
"plugins".to_string(),
serde_json::json!({ "enabled": true }),
);
let error = serde_json::from_value::<WorkerManifest>(nested).unwrap_err();
assert!(error.to_string().contains("unknown field `plugins`"));
}
#[test]
fn dynamic_plugin_feature_flag_is_rejected() {
let toml = format!("{MINIMAL_REQUIRED}\n[feature.plugins]\nenabled = true\n");
let error = WorkerManifest::from_toml(&toml).unwrap_err();
assert!(
error
.to_string()
.contains("dynamic Plugins are not supported"),
"unexpected error: {error}"
);
}
File diff suppressed because it is too large Load Diff
+45 -11
View File
@@ -18,7 +18,6 @@ use crate::config::{
CompactionConfigPartial, FeatureConfigPartial, PermissionConfigPartial, SessionConfigPartial,
};
use crate::model::{AuthRef, ModelManifest};
use crate::plugin::PluginConfig;
use crate::{
EngineManifestConfig, McpConfig, McpStdioCwdPolicy, Permission, ResolveError, ScopeConfig,
ScopeRule, SkillsConfig, WebConfig, WorkerManifest, WorkerManifestConfig, WorkerMetaConfig,
@@ -148,7 +147,6 @@ pub enum WorkspaceAuthorityRequirement {
MergeRequest,
Objective,
Orchestration,
Plugins,
Ticket,
Worker,
}
@@ -162,7 +160,6 @@ impl fmt::Display for WorkspaceAuthorityRequirement {
Self::MergeRequest => formatter.write_str("feature.merge_request"),
Self::Objective => formatter.write_str("feature.objective"),
Self::Orchestration => formatter.write_str("feature.orchestration"),
Self::Plugins => formatter.write_str("feature.plugins or plugin packages"),
Self::Ticket => formatter.write_str("feature.ticket"),
Self::Worker => formatter.write_str("feature.worker"),
}
@@ -202,9 +199,6 @@ pub fn validate_profile_execution_target(
if feature.orchestration.enabled {
requirements.insert(WorkspaceAuthorityRequirement::Orchestration);
}
if feature.plugins.enabled || !manifest.plugins.is_empty() {
requirements.insert(WorkspaceAuthorityRequirement::Plugins);
}
if feature.ticket.enabled
|| feature.ticket.authoring
|| feature.ticket.thread
@@ -638,7 +632,6 @@ fn resolve_profile_value(
session: profile.session,
permissions: profile.permissions,
feature: profile.feature,
plugins: profile.plugins,
mcp: profile.mcp,
compaction,
web: profile.web,
@@ -684,8 +677,6 @@ struct ProfileConfig {
#[serde(default)]
feature: FeatureConfigPartial,
#[serde(default)]
plugins: PluginConfig,
#[serde(default)]
mcp: McpConfig,
#[serde(default)]
compaction: Option<serde_json::Value>,
@@ -1267,6 +1258,51 @@ mod tests {
}
}
#[test]
fn ambient_plugin_directories_do_not_affect_builtin_profile_resolution() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("workspace/nested");
std::fs::create_dir_all(&workspace).unwrap();
for root in [tmp.path(), tmp.path().join("workspace").as_path()] {
let package = root.join(".yoi/plugins/broken.yoi-plugin");
std::fs::create_dir_all(package.parent().unwrap()).unwrap();
std::fs::write(package, b"malformed ambient package").unwrap();
}
let resolved = ProfileResolver::new()
.with_workspace_base(&workspace)
.resolve_for_target(
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, "default"),
ProfileResolveOptions::with_worker_name("standalone-worker"),
ProfileExecutionTarget::Standalone,
)
.unwrap();
assert_eq!(resolved.manifest.worker.name, "standalone-worker");
}
#[test]
fn profile_rejects_dynamic_plugin_configuration() {
let tmp = TempDir::new().unwrap();
for body in [
"[feature.plugins]\nenabled = true\n",
"[[plugins.enabled]]\nid = \"explicit:example\"\n",
] {
let profile = write_profile(tmp.path(), "plugin.toml", body);
let error = ProfileResolver::new()
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::path(profile),
ProfileResolveOptions::with_worker_name("runtime-worker"),
)
.unwrap_err();
assert!(
error.to_string().contains("unknown field"),
"unexpected error: {error}"
);
}
}
#[test]
fn builtin_default_resolves_as_a_standalone_local_capability_profile() {
let tmp = TempDir::new().unwrap();
@@ -1305,8 +1341,6 @@ mod tests {
assert!(!resolved.manifest.feature.flow.enabled);
assert!(!resolved.manifest.feature.worker.enabled);
assert!(!resolved.manifest.feature.manage_workdir.enabled);
assert!(!resolved.manifest.feature.plugins.enabled);
assert!(resolved.manifest.plugins.is_empty());
}
#[test]
-3
View File
@@ -43,7 +43,6 @@ memory = { workspace = true }
uuid = { workspace = true, features = ["v7"] }
session-metrics = { workspace = true }
arc-swap = "1.9.1"
wasmtime = { version = "45.0.2", default-features = false, features = ["std", "runtime", "cranelift", "component-model"] }
tungstenite = { version = "0.28.0", default-features = false, features = ["handshake", "native-tls", "url"] }
tokio-tungstenite = { version = "0.28.0", default-features = false, features = ["native-tls", "connect"] }
futures-util = { version = "0.3", features = ["sink"] }
@@ -53,5 +52,3 @@ dotenv = "0.15.0"
futures = { workspace = true }
serial_test = "3.4.0"
tempfile = { workspace = true }
wat = "1.241.2"
yoi-plugin-pdk = { workspace = true }
-6
View File
@@ -1385,12 +1385,6 @@ where
feature_registry
.add_module(crate::feature::builtin::orchestration::orchestration_feature());
}
for module in crate::feature::plugin::plugin_tool_features_if_enabled(
feature_config.plugins.enabled,
&worker.manifest().plugins,
) {
feature_registry = feature_registry.with_module(module);
}
if let Some(workspace_root) = local_workspace_root.as_ref() {
if let Some(module) =
crate::feature::mcp::discover_stdio_tool_feature(&mcp_config, workspace_root).await
+25 -1
View File
@@ -2211,7 +2211,6 @@ pub enum FeatureInstallError {
pub mod background;
pub mod builtin;
pub mod mcp;
pub mod plugin;
pub(crate) mod session;
#[cfg(test)]
@@ -2224,6 +2223,31 @@ mod tests {
use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn worker_feature_composition_has_no_dynamic_plugin_install_path() {
let feature_source = include_str!("feature.rs")
.split("#[cfg(test)]")
.next()
.unwrap();
let controller_source = include_str!("controller.rs")
.split("#[cfg(test)]")
.next()
.unwrap();
for forbidden in [
"pub mod plugin",
"plugin_tool_features_if_enabled",
"ResolvedPluginRecord",
"read_resolved_plugin_runtime_component",
"feature.plugins",
] {
assert!(
!feature_source.contains(forbidden) && !controller_source.contains(forbidden),
"dynamic Plugin install path returned through {forbidden}"
);
}
assert_eq!(FeatureId::builtin("task").as_str(), "builtin:task");
}
#[derive(Clone)]
struct DummyClient;
File diff suppressed because it is too large Load Diff
-2
View File
@@ -127,7 +127,6 @@ where
// parent manifest cannot accidentally grant its normal public tool surface
// or recursively schedule Feature-owned background work.
manifest.feature = Default::default();
manifest.plugins = Default::default();
manifest.mcp = Default::default();
manifest.skills = None;
manifest.compaction = None;
@@ -681,7 +680,6 @@ pub(crate) fn prepare_internal_worker_from_spec(
} = spec;
manifest.worker.name = format!("internal-{}-{}", identity.kind, identity.run_id);
manifest.feature = Default::default();
manifest.plugins = Default::default();
manifest.mcp = Default::default();
manifest.skills = None;
manifest.compaction = None;
-1
View File
@@ -926,7 +926,6 @@ fn manifest_to_reusable_config(manifest: &WorkerManifest) -> WorkerManifestConfi
rules: p.rules.clone(),
}),
feature: manifest.feature.clone().into(),
plugins: manifest.plugins.clone(),
mcp: manifest.mcp.clone(),
compaction: manifest
.compaction
-74
View File
@@ -7750,80 +7750,6 @@ permission = "read"
})
);
}
#[test]
fn plugin_resolved_manifest_snapshot_is_persisted_without_profile() {
let mut manifest = WorkerManifest::from_toml(
r#"
[worker]
name = "plugin-snapshot"
[model]
scheme = "anthropic"
model_id = "claude-sonnet-4-20250514"
[engine]
instruction = "saved"
[[scope.allow]]
target = "/snapshot/workspace"
permission = "read"
"#,
)
.unwrap();
assert!(manifest.profile.is_none());
assert!(
worker_metadata_for_manifest(&manifest, None, None, None)
.resolved_manifest_snapshot
.is_none()
);
manifest.plugins.resolved = vec![manifest::plugin::ResolvedPluginRecord {
identity: manifest::plugin::SourceQualifiedPluginId::new(
manifest::plugin::PluginSourceKind::Project,
"example",
),
source: manifest::plugin::PluginSourceKind::Project,
package_path: PathBuf::from("/snapshot/workspace/.yoi/plugins/example.yoi-plugin"),
package_label: "example.yoi-plugin".to_string(),
digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
.to_string(),
version: "0.1.0".to_string(),
manifest: manifest::plugin::PluginPackageManifest {
schema_version: 1,
id: "example".to_string(),
name: "Example".to_string(),
version: "0.1.0".to_string(),
description: None,
surfaces: vec![manifest::plugin::PluginSurface::Hook],
runtime: None,
hooks: vec![],
tools: vec![],
services: vec![],
ingresses: vec![],
permissions: vec![],
request: vec![],
websocket: vec![],
},
enabled_surfaces: vec![manifest::plugin::PluginSurface::Hook],
grants: manifest::plugin::PluginGrantConfig::default(),
config: None,
}];
let metadata = worker_metadata_for_manifest(&manifest, None, None, None);
let snapshot = metadata
.resolved_manifest_snapshot
.expect("plugin-resolved manifest should be snapshotted");
let restored = manifest::read_persisted_worker_manifest_snapshot(snapshot).unwrap();
assert!(restored.profile.is_none());
assert_eq!(restored.plugins.resolved.len(), 1);
assert_eq!(
restored.plugins.resolved[0].digest,
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
assert_eq!(restored.plugins.resolved[0].version, "0.1.0");
}
}
#[cfg(test)]
+19 -74
View File
@@ -1345,7 +1345,7 @@ async fn run_login(backend_url: &str, no_wait: bool) -> Result<(), ParseError> {
fn parse_plugin_args(args: &[String]) -> Result<plugin_cli::PluginCliCommand, ParseError> {
let Some((subcommand, rest)) = args.split_first() else {
return Err(ParseError(
"yoi plugin requires `new`, `check`, `pack`, `list`, or `show <ref>`".to_string(),
"yoi plugin requires `new`, `check`, or `pack`".to_string(),
));
};
match subcommand.as_str() {
@@ -1397,30 +1397,6 @@ fn parse_plugin_args(args: &[String]) -> Result<plugin_cli::PluginCliCommand, Pa
)),
}
}
"list" => {
let (plugin_args, positional) = parse_plugin_common_args(rest)?;
if !positional.is_empty() {
return Err(ParseError(
"yoi plugin list does not accept positional arguments".to_string(),
));
}
Ok(plugin_cli::PluginCliCommand::List(plugin_args))
}
"show" => {
let (plugin_args, positional) = parse_plugin_common_args(rest)?;
match positional.as_slice() {
[reference] => Ok(plugin_cli::PluginCliCommand::Show {
reference: reference.clone(),
args: plugin_args,
}),
[] => Err(ParseError(
"yoi plugin show requires a plugin ref".to_string(),
)),
_ => Err(ParseError(
"yoi plugin show accepts exactly one plugin ref".to_string(),
)),
}
}
"--help" | "-h" => Err(ParseError(plugin_usage().to_string())),
other => Err(ParseError(format!(
"unknown yoi plugin subcommand `{other}`"
@@ -1438,35 +1414,7 @@ fn parse_plugin_common_args(
let arg = &args[index];
match arg.as_str() {
"--json" => parsed.json = true,
"--workspace" => {
index += 1;
let Some(value) = args.get(index) else {
return Err(ParseError("--workspace requires a value".to_string()));
};
parsed.workspace = Some(PathBuf::from(value));
}
"--profile" => {
index += 1;
let Some(value) = args.get(index) else {
return Err(ParseError("--profile requires a value".to_string()));
};
parsed.profile = Some(value.clone());
}
"--help" | "-h" => return Err(ParseError(plugin_usage().to_string())),
_ if arg.starts_with("--workspace=") => {
let value = arg.trim_start_matches("--workspace=");
if value.is_empty() {
return Err(ParseError("--workspace requires a value".to_string()));
}
parsed.workspace = Some(PathBuf::from(value));
}
_ if arg.starts_with("--profile=") => {
let value = arg.trim_start_matches("--profile=");
if value.is_empty() {
return Err(ParseError("--profile requires a value".to_string()));
}
parsed.profile = Some(value.to_string());
}
_ if arg.starts_with('-') => {
return Err(ParseError(format!("unknown yoi plugin option `{arg}`")));
}
@@ -1506,7 +1454,7 @@ fn parse_plugin_pack_args(
}
fn plugin_usage() -> &'static str {
"usage: yoi plugin new <rust-component-tool|rust-component-service> <path-or-name> [--json]\n yoi plugin check <path-or-package> [--json]\n yoi plugin pack <path> [--output <file>] [--json]\n yoi plugin list [--workspace PATH] [--profile REF] [--json]\n yoi plugin show <ref> [--workspace PATH] [--profile REF] [--json]"
"usage: yoi plugin new <rust-component-tool|rust-component-service> <path-or-name> [--json]\n yoi plugin check <path-or-package> [--json]\n yoi plugin pack <path> [--output <file>] [--json]"
}
fn parse_mcp_args(args: &[String]) -> Result<mcp_cli::McpCliCommand, ParseError> {
@@ -2654,29 +2602,26 @@ backend = "shared"
}
#[test]
fn parse_plugin_list_and_show() {
match parse_args_from(["plugin", "list", "--workspace=/tmp/ws", "--json"]).unwrap() {
Mode::Plugin(plugin_cli::PluginCliCommand::List(options)) => {
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
assert!(options.json);
}
_ => panic!("expected Plugin list mode"),
fn plugin_cli_rejects_ambient_catalog_commands_and_options() {
for args in [
vec!["plugin", "list"],
vec!["plugin", "show", "project:echo"],
vec!["plugin", "check", "plugin", "--workspace=/tmp/ws"],
vec!["plugin", "check", "plugin", "--profile", "project:inspect"],
] {
let error = parse_args_from(args).unwrap_err();
assert!(
error.0.contains("unknown yoi plugin"),
"unexpected error: {error}"
);
}
match parse_args_from([
"plugin",
"show",
"project:echo",
"--profile",
"project:inspect",
])
.unwrap()
{
Mode::Plugin(plugin_cli::PluginCliCommand::Show { reference, args }) => {
assert_eq!(reference, "project:echo");
assert_eq!(args.profile.as_deref(), Some("project:inspect"));
match parse_args_from(["plugin", "check", "plugin.yoi-plugin", "--json"]).unwrap() {
Mode::Plugin(plugin_cli::PluginCliCommand::Check { input, args }) => {
assert_eq!(input, PathBuf::from("plugin.yoi-plugin"));
assert!(args.json);
}
_ => panic!("expected Plugin show mode"),
_ => panic!("expected Plugin check mode"),
}
}
File diff suppressed because it is too large Load Diff