#![cfg(feature = "test-helpers")]
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, OnceLock};
use yosh::env::ShellEnv;
use yosh::plugin::{PluginExec, PluginManager, test_helpers};
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn lock_test() -> std::sync::MutexGuard<'static, ()> {
TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
static TEST_PLUGIN_WASM: OnceLock<PathBuf> = OnceLock::new();
static TRAP_PLUGIN_WASM: OnceLock<PathBuf> = OnceLock::new();
static SLOW_PLUGIN_WASM: OnceLock<PathBuf> = OnceLock::new();
static PERF_PLUGIN_WASM: OnceLock<PathBuf> = OnceLock::new();
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).into()
}
fn ensure_built(crate_name: &str, slot: &OnceLock<PathBuf>) -> PathBuf {
slot.get_or_init(|| {
let status = Command::new("cargo")
.args([
"component",
"build",
"-p",
crate_name,
"--target",
"wasm32-wasip2",
"--release",
])
.status()
.expect("cargo component build failed (is cargo-component installed?)");
assert!(status.success(), "{} build failed", crate_name);
workspace_root().join(format!("target/wasm32-wasip2/release/{}.wasm", crate_name))
})
.clone()
}
fn test_plugin_wasm() -> PathBuf {
ensure_built("test_plugin", &TEST_PLUGIN_WASM)
}
fn trap_plugin_wasm() -> PathBuf {
ensure_built("trap_plugin", &TRAP_PLUGIN_WASM)
}
fn slow_plugin_wasm() -> PathBuf {
ensure_built("slow_plugin", &SLOW_PLUGIN_WASM)
}
fn perf_plugin_wasm() -> PathBuf {
ensure_built("perf_plugin", &PERF_PLUGIN_WASM)
}
fn fresh_env() -> ShellEnv {
ShellEnv::new("yosh", vec![])
}
#[test]
fn t01_capability_allowlist_applied_to_linker() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_VARIABLES_READ | yosh_plugin_api::CAP_IO;
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, allowed, &[])
.expect("load test_plugin with restricted caps");
env.vars
.set("YOSH_TEST_VAR", "abc")
.expect("set sentinel var");
let exec = mgr.exec_command(&mut env, "echo_var", &["YOSH_TEST_VAR".into()]);
assert!(
matches!(exec, PluginExec::Handled(0)),
"echo_var with read+io grant must Handled(0), got {:?}",
exec
);
}
#[test]
fn t02_wasm_trap_isolation_via_with_env() {
let _g = lock_test();
let wasm = trap_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, yosh_plugin_api::CAP_ALL, &[])
.expect("load trap_plugin");
let r1 = mgr.exec_command(&mut env, "trap_now", &[]);
assert!(
matches!(r1, PluginExec::Failed),
"first call must Failed (trap caught); got {:?}",
r1
);
let r2 = mgr.exec_command(&mut env, "trap_now", &[]);
assert!(
matches!(r2, PluginExec::Failed),
"second call must remain Failed (instance invalidated); got {:?}",
r2
);
}
#[test]
fn t03_with_env_resets_env_after_dispatch() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, yosh_plugin_api::CAP_ALL, &[])
.expect("load test_plugin");
assert_eq!(
test_helpers::env_pointer_is_null_in_store(&mgr),
Some(true),
"env pointer must be null after on_load returns"
);
env.vars.set("X", "1").expect("set X");
let _ = mgr.exec_command(&mut env, "echo_var", &["X".into()]);
assert_eq!(
test_helpers::env_pointer_is_null_in_store(&mgr),
Some(true),
"env pointer must be null after first exec"
);
let _ = mgr.exec_command(&mut env, "echo_var", &["X".into()]);
assert_eq!(
test_helpers::env_pointer_is_null_in_store(&mgr),
Some(true),
"env pointer must be null after second exec"
);
}
#[test]
fn t04_metadata_contract_covered_by_host_unit_tests() {
assert!(true);
}
#[test]
fn t05_on_load_has_host_api_access() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, yosh_plugin_api::CAP_ALL, &[])
.expect("load test_plugin");
let exec = mgr.exec_command(&mut env, "dump_events", &[]);
assert!(
matches!(exec, PluginExec::Handled(0)),
"dump_events must Handled(0); got {:?}",
exec
);
let log = env
.vars
.get("YOSH_TEST_EVENT_LOG")
.map(|s| s.to_string())
.unwrap_or_default();
assert!(
log.contains("on_load"),
"event log must contain 'on_load' (was {:?})",
log
);
}
#[test]
fn t10_wasi_lockdown_covered_by_linker_unit_test() {
assert!(true);
}
#[test]
fn t11_unknown_capability_warning_covered_by_unit_tests() {
let result = yosh_plugin_api::parse_capability("variables:execute");
assert!(result.is_none(), "unknown capability string returns None");
}
#[test]
fn t12_required_vs_granted_parity_warning_data_path() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_VARIABLES_READ | yosh_plugin_api::CAP_IO;
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, allowed, &[])
.expect("load with restricted caps must still succeed");
env.vars.set("PARITY", "ok").expect("set sentinel");
let exec = mgr.exec_command(&mut env, "echo_var", &["PARITY".into()]);
assert!(
matches!(exec, PluginExec::Handled(0)),
"granted read+io path still works"
);
}
#[test]
fn t13_hook_dispatch_suppression_for_non_overridden_post_exec() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, yosh_plugin_api::CAP_ALL, &[])
.expect("load test_plugin");
let exec = mgr.exec_command(&mut env, "set_post_exec_marker", &[]);
assert!(matches!(exec, PluginExec::Handled(0)));
assert_eq!(
env.vars.get("YOSH_TEST_POST_EXEC_FIRED"),
Some("0"),
"sentinel must be seeded to '0' before invocation"
);
mgr.call_post_exec(&mut env, "echo hello", 0);
assert_eq!(
env.vars.get("YOSH_TEST_POST_EXEC_FIRED"),
Some("0"),
"post_exec must NOT have fired (implemented_hooks lacks PostExec)"
);
let exec = mgr.exec_command(&mut env, "dump_events", &[]);
assert!(matches!(exec, PluginExec::Handled(0)));
let log = env
.vars
.get("YOSH_TEST_EVENT_LOG")
.map(|s| s.to_string())
.unwrap_or_default();
assert!(
!log.contains("post_exec:"),
"event log must NOT contain 'post_exec:' entry (was {:?})",
log
);
}
#[test]
fn t13b_implemented_hook_does_fire() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, yosh_plugin_api::CAP_ALL, &[])
.expect("load test_plugin");
mgr.call_pre_exec(&mut env, "ls -la");
let exec = mgr.exec_command(&mut env, "dump_events", &[]);
assert!(matches!(exec, PluginExec::Handled(0)));
let log = env
.vars
.get("YOSH_TEST_EVENT_LOG")
.map(|s| s.to_string())
.unwrap_or_default();
assert!(
log.contains("pre_exec:ls -la"),
"event log must contain 'pre_exec:ls -la' (was {:?})",
log
);
}
#[test]
fn t14_linker_construction_smoke_covered_by_unit_test() {
assert!(true);
}
#[test]
fn t06_cwasm_missing_falls_back_to_in_memory() {
use yosh::plugin::cache::CacheKey;
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = ShellEnv::new("yosh", Vec::new());
let mut mgr = PluginManager::new();
let wasm_bytes = std::fs::read(&wasm).expect("read wasm");
let wasm_sha = yosh::plugin::cache::sha256_hex(&wasm_bytes);
let key = CacheKey::for_runtime(
wasm_sha,
yosh_plugin_manager::precompile::ENGINE_FINGERPRINT,
);
let nonexistent_cwasm = wasm.with_extension("nonexistent.cwasm");
test_helpers::load_plugin_with_cache(
&mut mgr,
&wasm,
&mut env,
yosh_plugin_api::CAP_ALL,
&nonexistent_cwasm,
&key,
&[],
)
.expect("load with stale cwasm path must fall back, not fail");
let exec = mgr.exec_command(&mut env, "test_cmd", &["smoke".into()]);
assert!(
matches!(exec, PluginExec::Handled(0)),
"plugin must work after cwasm fallback"
);
}
#[test]
fn t09_wasm_sha_mismatch_refuses_to_load() {
use yosh::plugin::cache::CacheKey;
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = ShellEnv::new("yosh", Vec::new());
let mut mgr = PluginManager::new();
let bogus_sha = "0".repeat(64);
let key = CacheKey::for_runtime(
bogus_sha,
yosh_plugin_manager::precompile::ENGINE_FINGERPRINT,
);
let nonexistent_cwasm = wasm.with_extension("nonexistent.cwasm");
let result = test_helpers::load_plugin_with_cache(
&mut mgr,
&wasm,
&mut env,
yosh_plugin_api::CAP_ALL,
&nonexistent_cwasm,
&key,
&[],
);
assert!(result.is_err(), "load with bad expected SHA must fail");
let msg = result.unwrap_err();
assert!(
msg.contains("wasm SHA-256 mismatch"),
"error must mention SHA-256 mismatch (was {:?})",
msg
);
}
#[test]
fn t15_files_read_granted_works() {
let _g = lock_test();
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("hello.txt");
std::fs::write(&path, b"YOSH_TEST_CONTENT\n").expect("write fixture");
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_FILES_READ;
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, allowed, &[])
.expect("load test_plugin with files:read");
let exec = mgr.exec_command(
&mut env,
"read-file",
&[path.to_string_lossy().into_owned()],
);
assert!(
matches!(exec, PluginExec::Handled(0)),
"read-file with files:read grant must Handled(0), got {:?}",
exec
);
}
#[test]
fn t16_files_read_denied_returns_error() {
let _g = lock_test();
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("hello.txt");
std::fs::write(&path, b"YOSH_TEST_CONTENT\n").expect("write fixture");
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_VARIABLES_READ;
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, allowed, &[])
.expect("load test_plugin without files:read");
let exec = mgr.exec_command(
&mut env,
"read-file",
&[path.to_string_lossy().into_owned()],
);
assert!(
matches!(exec, PluginExec::Handled(13)),
"read-file without files:read grant must Handled(13) (Denied), got {:?}",
exec
);
}
#[test]
fn t17_files_write_granted_works() {
let _g = lock_test();
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("out.txt");
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_FILES_WRITE;
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, allowed, &[])
.expect("load test_plugin with files:write");
let exec = mgr.exec_command(
&mut env,
"write-file",
&[path.to_string_lossy().into_owned()],
);
assert!(
matches!(exec, PluginExec::Handled(0)),
"write-file with files:write grant must Handled(0), got {:?}",
exec
);
let written = std::fs::read(&path).expect("read written file");
assert_eq!(
written, b"YOSH_TEST_CONTENT\n",
"host-side read of plugin-written file must match canonical marker",
);
}
#[test]
fn t18_files_write_denied_returns_error() {
let _g = lock_test();
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("out.txt");
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_VARIABLES_READ;
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, allowed, &[])
.expect("load test_plugin without files:write");
let exec = mgr.exec_command(
&mut env,
"write-file",
&[path.to_string_lossy().into_owned()],
);
assert!(
matches!(exec, PluginExec::Handled(13)),
"write-file without files:write grant must Handled(13) (Denied), got {:?}",
exec
);
assert!(!path.exists(), "deny stub must not create the file");
}
#[test]
fn t19_files_read_only_blocks_write() {
let _g = lock_test();
let dir = tempfile::tempdir().expect("tempdir");
let read_path = dir.path().join("in.txt");
let write_path = dir.path().join("out.txt");
std::fs::write(&read_path, b"YOSH_TEST_CONTENT\n").expect("write fixture");
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_FILES_READ; test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, allowed, &[])
.expect("load test_plugin with files:read only");
let r = mgr.exec_command(
&mut env,
"read-file",
&[read_path.to_string_lossy().into_owned()],
);
assert!(
matches!(r, PluginExec::Handled(0)),
"read-file with files:read grant must Handled(0), got {:?}",
r
);
let w = mgr.exec_command(
&mut env,
"write-file",
&[write_path.to_string_lossy().into_owned()],
);
assert!(
matches!(w, PluginExec::Handled(13)),
"write-file without files:write grant must Handled(13), got {:?}",
w
);
assert!(!write_path.exists(), "deny stub must not create the file");
}
#[test]
fn t20_commands_exec_granted_with_pattern_works() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_COMMANDS_EXEC | yosh_plugin_api::CAP_IO;
test_helpers::load_plugin_with_caps(
&mut mgr,
&wasm,
&mut env,
allowed,
&["echo:*".to_string()],
)
.expect("load test_plugin with commands:exec + echo:* allowlist");
let exec = mgr.exec_command(&mut env, "run-echo", &["hello".into()]);
assert!(
matches!(exec, PluginExec::Handled(0)),
"run-echo with allowed pattern must Handled(0), got {:?}",
exec
);
}
#[test]
fn t21_commands_exec_denied_without_capability() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_IO;
test_helpers::load_plugin_with_caps(
&mut mgr,
&wasm,
&mut env,
allowed,
&["echo:*".to_string()],
)
.expect("load without commands:exec");
let exec = mgr.exec_command(&mut env, "run-echo", &["hi".into()]);
assert!(
matches!(exec, PluginExec::Handled(100)),
"run-echo without capability must map to exit 100 (Denied), got {:?}",
exec
);
}
#[test]
fn t22_commands_exec_pattern_not_allowed_without_match() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_COMMANDS_EXEC | yosh_plugin_api::CAP_IO;
test_helpers::load_plugin_with_caps(
&mut mgr,
&wasm,
&mut env,
allowed,
&["ls:*".to_string()],
)
.expect("load with non-matching allowlist");
let exec = mgr.exec_command(&mut env, "run-echo", &["hi".into()]);
assert!(
matches!(exec, PluginExec::Handled(101)),
"run-echo without matching pattern must map to exit 101 (PatternNotAllowed), got {:?}",
exec
);
}
#[test]
fn t23_commands_exec_exact_pattern_rejects_extra_args() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_COMMANDS_EXEC | yosh_plugin_api::CAP_IO;
test_helpers::load_plugin_with_caps(
&mut mgr,
&wasm,
&mut env,
allowed,
&["echo".to_string()],
)
.expect("load with exact-length allowlist");
let exec = mgr.exec_command(&mut env, "run-echo", &["hi".into()]);
assert!(
matches!(exec, PluginExec::Handled(101)),
"run-echo with extra args under exact pattern must map to exit 101, got {:?}",
exec
);
}
#[test]
fn t24_commands_exec_invalid_pattern_fails_plugin_load() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let allowed = yosh_plugin_api::CAP_COMMANDS_EXEC | yosh_plugin_api::CAP_IO;
let result = test_helpers::load_plugin_with_caps(
&mut mgr,
&wasm,
&mut env,
allowed,
&[":*".to_string()],
);
assert!(
result.is_err(),
"load_plugin_with_caps should fail on invalid pattern, got Ok"
);
let err = result.unwrap_err();
assert!(
err.contains("invalid allowed_commands pattern"),
"error must mention the offending field, got: {}",
err
);
}
#[test]
fn t25_pre_prompt_timeout_invalidates_slow_plugin() {
use std::time::Instant;
let _g = lock_test();
let wasm = slow_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
test_helpers::set_pre_prompt_timeout_for_tests(&mut mgr, 100);
test_helpers::load_plugin_with_caps(
&mut mgr,
&wasm,
&mut env,
yosh_plugin_api::CAP_HOOK_PRE_PROMPT | yosh_plugin_api::CAP_HOOK_PRE_EXEC,
&[],
)
.expect("load slow_plugin with pre_prompt+pre_exec caps");
let t0 = Instant::now();
mgr.call_pre_prompt(&mut env);
let first_elapsed = t0.elapsed();
assert!(
first_elapsed.as_millis() < 1_000,
"first call_pre_prompt should be bounded by the deadline; got {:?}",
first_elapsed
);
let t1 = Instant::now();
mgr.call_pre_prompt(&mut env);
let second_elapsed = t1.elapsed();
assert!(
second_elapsed.as_millis() < 50,
"second call_pre_prompt should be a fast skip; got {:?}",
second_elapsed
);
}
#[test]
fn perf_plugin_commands_exit_zero() {
let _g = lock_test();
let wasm = perf_plugin_wasm();
let mut env = fresh_env();
env.vars
.set("PERF_VAR", "perf_value")
.expect("set PERF_VAR");
let mut mgr = PluginManager::new();
test_helpers::load_plugin_with_caps(
&mut mgr,
&wasm,
&mut env,
yosh_plugin_api::CAP_ALL,
&[],
)
.expect("load perf_plugin");
for cmd in ["noop_cmd", "noop_var", "burst_var"] {
let result = mgr.exec_command(&mut env, cmd, &[]);
assert!(
matches!(result, PluginExec::Handled(0)),
"{} should be Handled(0), got {:?}",
cmd,
result
);
}
}
#[test]
fn perf_plugin_three_aliases_load_independently() {
use std::io::Write;
let _guard = lock_test();
let wasm = perf_plugin_wasm();
let tmp = tempfile::tempdir().expect("tempdir");
let lock_path = tmp.path().join("plugins.lock");
let mut f = std::fs::File::create(&lock_path).expect("create lock");
writeln!(
f,
r#"
[[plugin]]
name = "perf_a"
path = "{wasm}"
enabled = true
capabilities = ["variables:read", "hooks:pre_prompt", "hooks:pre_exec", "hooks:post_exec"]
[[plugin]]
name = "perf_b"
path = "{wasm}"
enabled = true
capabilities = ["variables:read", "hooks:pre_prompt", "hooks:pre_exec", "hooks:post_exec"]
[[plugin]]
name = "perf_c"
path = "{wasm}"
enabled = true
capabilities = ["variables:read", "hooks:pre_prompt", "hooks:pre_exec", "hooks:post_exec"]
"#,
wasm = wasm.display()
)
.expect("write lock");
drop(f);
let mut env = fresh_env();
let mut mgr = PluginManager::new();
mgr.load_from_config(&lock_path, &mut env);
mgr.call_pre_prompt(&mut env);
mgr.call_pre_exec(&mut env, "noop");
}
#[test]
fn perf_plugin_hooks_dispatch_without_panic() {
let _g = lock_test();
let wasm = perf_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
test_helpers::load_plugin_with_caps(
&mut mgr,
&wasm,
&mut env,
yosh_plugin_api::CAP_ALL,
&[],
)
.expect("load perf_plugin");
mgr.call_pre_prompt(&mut env);
mgr.call_pre_exec(&mut env, "noop");
mgr.call_post_exec(&mut env, "noop", 0);
}
#[test]
fn linker_cache_reuses_entry_for_same_mask() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let caps = yosh_plugin_api::CAP_ALL;
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, caps, &[])
.expect("first load");
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, caps, &[])
.expect("second load");
let len = test_helpers::linker_cache_len(&mgr);
assert_eq!(
len,
2,
"expected 2 entries (CAP_ALL scratch + shared real mask), got {len}",
);
}
#[test]
fn linker_cache_separates_entries_for_distinct_masks() {
let _g = lock_test();
let wasm = test_plugin_wasm();
let mut env = fresh_env();
let mut mgr = PluginManager::new();
let caps_a = yosh_plugin_api::CAP_VARIABLES_READ;
let caps_b = yosh_plugin_api::CAP_VARIABLES_READ | yosh_plugin_api::CAP_FILESYSTEM;
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, caps_a, &[])
.expect("load a");
test_helpers::load_plugin_with_caps(&mut mgr, &wasm, &mut env, caps_b, &[])
.expect("load b");
let len = test_helpers::linker_cache_len(&mgr);
assert_eq!(
len,
3,
"expected 3 entries (CAP_ALL scratch + 2 distinct real masks), got {len}",
);
}