Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ tests/output
# Ignore Rust target directory
target/

# Ignore worktrees
worktrees/

# AI things
.claude
CLAUDE.md
93 changes: 74 additions & 19 deletions crates/wash/src/cli/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::{path::PathBuf, sync::Arc};
use std::{path::Path, sync::Arc};

use anyhow::Context;
use anyhow::bail;
Expand All @@ -20,6 +20,7 @@ use wasmtime_wasi_http::io::TokioIo;
use crate::runtime::bindings::plugin::wasmcloud::wash::types::Metadata;
use crate::{
cli::{CliCommand, CliContext, CommandOutput, OutputKind, component_build::build_component},
oci::{OCI_CACHE_DIR, OciConfig, pull_component},
plugin::{
InstallPluginOptions, PluginComponent, install_plugin, list_plugins, uninstall_plugin,
},
Expand Down Expand Up @@ -275,9 +276,9 @@ pub struct ListCommand {

#[derive(Args, Debug, Clone)]
pub struct TestCommand {
/// Path to the component or component project to test
/// Plugin reference, which can be a local file path, project directory, or remote OCI reference
#[clap(name = "plugin")]
pub plugin: PathBuf,
pub plugin: String,
/// The hook types to test
#[clap(name = "type", long = "hook", conflicts_with = "arg")]
pub hooks: Vec<HookType>,
Expand Down Expand Up @@ -408,22 +409,76 @@ impl TestCommand {
/// Handle the plugin test command
#[instrument(level = "debug", skip_all, name = "plugin_test")]
pub async fn handle(&self, ctx: &CliContext) -> anyhow::Result<CommandOutput> {
let wasm = if self.plugin.is_dir() {
let config = ctx
.ensure_config(Some(self.plugin.as_path()))
.await
.context("failed to load config")?;
let built_path = build_component(&self.plugin, ctx, &config)
.await
.context("Failed to build component from directory")?;
tokio::fs::read(&built_path.artifact_path)
.await
.context("Failed to read built component file")?
let path = Path::new(&self.plugin);

let wasm = if path.exists() {
if path.is_file() {
// Direct file path - load it
debug!(plugin_ref = ?self.plugin, "loading plugin from file");
tokio::fs::read(&self.plugin)
.await
.context("Failed to read component file")?
} else if path.is_dir() {
// Directory - check if it's a project and build it
debug!(
plugin_ref = ?self.plugin,
"directory detected, checking if it's a project"
);

// Check for project files
let project_files = [
"Cargo.toml",
"go.mod",
"package.json",
"wasmcloud.toml",
".wash/config.json",
];
let is_project = project_files.iter().any(|file| path.join(file).exists());

if is_project {
debug!(
plugin_ref = ?self.plugin,
"project directory detected, building component"
);

let config = ctx
.ensure_config(Some(path))
.await
.context("Failed to load project configuration")?;

let built_path = build_component(path, ctx, &config)
.await
.context("Failed to build component from project directory")?;

debug!(artifact_path = ?built_path.artifact_path, "Component built successfully");

tokio::fs::read(&built_path.artifact_path)
.await
.context("Failed to read built component file")?
} else {
return Err(anyhow::anyhow!(
"Directory '{}' does not appear to be a project (no Cargo.toml, go.mod, package.json, wasmcloud.toml, or .wash/config.json found)",
self.plugin
));
}
} else {
return Err(anyhow::anyhow!(
"Path '{}' exists but is neither a file nor directory",
self.plugin
));
}
} else {
tokio::fs::read(&self.plugin)
.await
.context("Failed to read component file")?
// TODO(#14): support OCI references too
debug!(
plugin_ref = ?self.plugin,
"Path does not exist locally, attempting to pull from remote"
);
// Pull component from remote
pull_component(
&self.plugin,
OciConfig::new_with_cache(ctx.cache_dir().join(OCI_CACHE_DIR)),
)
.await
.context("Failed to pull plugin from remote")?
};

let mut output = String::new();
Expand Down Expand Up @@ -484,7 +539,7 @@ impl TestCommand {
Ok(CommandOutput::ok(
output,
Some(json!({
"plugin_path": self.plugin.to_string_lossy(),
"plugin_path": self.plugin,
"args": self.args,
"hooks": self.hooks,
"metadata": component.metadata,
Expand Down
6 changes: 3 additions & 3 deletions tests/integration_plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ async fn test_plugin_test_inspect_comprehensive() -> Result<()> {
};

let test_cmd_with_command = TestCommand {
plugin: inspect_plugin_path.clone(),
plugin: inspect_plugin_path.to_string_lossy().to_string(),
args: vec![component_arg.clone()],
hooks: vec![],
};
Expand Down Expand Up @@ -118,7 +118,7 @@ async fn test_plugin_test_inspect_comprehensive() -> Result<()> {
// Test 3: Plugin test with --hook afterdev
eprintln!("🔍 Test 3: Plugin test with AfterDev hook");
let test_cmd_with_hook = TestCommand {
plugin: inspect_plugin_path.clone(),
plugin: inspect_plugin_path.to_string_lossy().to_string(),
args: vec![],
hooks: vec![HookType::AfterDev],
};
Expand All @@ -138,7 +138,7 @@ async fn test_plugin_test_inspect_comprehensive() -> Result<()> {
// Test 4: Plugin test with both command and AfterDev hook
eprintln!("🔍 Test 4: Plugin test with both component inspection and AfterDev hook");
let test_cmd_with_both = TestCommand {
plugin: inspect_plugin_path.clone(),
plugin: inspect_plugin_path.to_string_lossy().to_string(),
args: vec![component_arg.clone()],
hooks: vec![HookType::AfterDev],
};
Expand Down
Loading