a31eb59678
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
150 lines
7.0 KiB
Rust
150 lines
7.0 KiB
Rust
//! The good plugin end to end: lifecycle, commands, permissions and the plugin set.
|
|
|
|
mod common;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use common::{StubHost, load, manifest_for};
|
|
use notescraft_plugins::{Error, Limits, Manifest, Plugin, PluginSet};
|
|
|
|
const READ: [&str; 1] = ["vault.read"];
|
|
|
|
#[test]
|
|
fn loads_and_reports_its_manifest() {
|
|
let plugin = load("test-plugin", "good", &READ, &["echo"], Arc::new(StubHost::new())).unwrap_or_else(|e| panic!("{e}"));
|
|
assert_eq!(plugin.id(), "test-plugin");
|
|
assert_eq!(plugin.manifest().name, "Test plugin");
|
|
assert_eq!(plugin.limits(), &Limits::default());
|
|
assert!(!plugin.is_disabled());
|
|
}
|
|
|
|
#[test]
|
|
fn run_command_echoes_args_and_reaches_the_host() {
|
|
let host = Arc::new(StubHost::new());
|
|
let mut plugin = load("test-plugin", "good", &READ, &["echo"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}"));
|
|
let out = plugin.run_command("echo", r#"{"n":1}"#).unwrap_or_else(|e| panic!("{e}"));
|
|
assert_eq!(out, r#"{"n":1}"#);
|
|
assert_eq!(host.calls(), vec![String::from("log(info,running)"), String::from("vault_read(notes/a.md)")]);
|
|
}
|
|
|
|
#[test]
|
|
fn lifecycle_runs_load_unload_and_events() {
|
|
let host = Arc::new(StubHost::new());
|
|
let mut plugin = load("test-plugin", "good", &READ, &["echo"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}"));
|
|
plugin.on_load().unwrap_or_else(|e| panic!("{e}"));
|
|
plugin.send_event(r#"{"type":"note.opened"}"#).unwrap_or_else(|e| panic!("{e}"));
|
|
plugin.on_unload().unwrap_or_else(|e| panic!("{e}"));
|
|
assert_eq!(host.calls(), vec![String::from("log(info,running)"), String::from("notice(goodbye)")]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_call_without_its_permission_never_reaches_the_host() {
|
|
let host = Arc::new(StubHost::new());
|
|
let mut plugin = load("test-plugin", "good", &[], &["echo"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}"));
|
|
let err = plugin.run_command("echo", "{}").unwrap_err();
|
|
assert!(matches!(err, Error::PermissionDenied { call: "vault_read", .. }), "{err}");
|
|
// `ns.log` ran first and is always allowed; the read never did.
|
|
assert_eq!(host.calls(), vec![String::from("log(info,running)")]);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_arguments_are_accepted() {
|
|
let host = Arc::new(StubHost::new());
|
|
let mut plugin = load("test-plugin", "good", &READ, &["echo"], host as Arc<_>).unwrap_or_else(|e| panic!("{e}"));
|
|
assert_eq!(plugin.run_command("echo", "").unwrap_or_else(|e| panic!("{e}")), "");
|
|
}
|
|
|
|
#[test]
|
|
fn a_declared_permission_lets_the_call_through() {
|
|
let host = Arc::new(StubHost::new());
|
|
let mut plugin = load("test-plugin", "good", &["vault.read"], &["echo"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}"));
|
|
plugin.run_command("echo", "{}").unwrap_or_else(|e| panic!("{e}"));
|
|
assert!(host.calls().contains(&String::from("vault_read(notes/a.md)")));
|
|
}
|
|
|
|
#[test]
|
|
fn a_failing_host_call_is_reported_with_its_message() {
|
|
let host = Arc::new(StubHost::failing());
|
|
let mut plugin = load("test-plugin", "good", &READ, &["echo"], host as Arc<_>).unwrap_or_else(|e| panic!("{e}"));
|
|
let err = plugin.run_command("echo", "{}").unwrap_err();
|
|
assert!(matches!(&err, Error::HostCall { call: "log", message, .. } if message == "the vault is locked"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn an_oversize_host_answer_hits_the_result_cap() {
|
|
let host = Arc::new(StubHost::with_payload(&"x".repeat(5 << 20)));
|
|
let mut plugin = load("test-plugin", "huge_host_answer", &[], &["now"], host as Arc<_>).unwrap_or_else(|e| panic!("{e}"));
|
|
let err = plugin.run_command("now", "{}").unwrap_err();
|
|
assert!(matches!(err, Error::PluginLimit(_)), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn vault_write_needs_its_own_permission() {
|
|
let host = Arc::new(StubHost::new());
|
|
let mut plugin = load("test-plugin", "write", &["vault.read"], &["write"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}"));
|
|
let err = plugin.run_command("write", "body").unwrap_err();
|
|
assert!(matches!(&err, Error::PermissionDenied { call: "vault_write", .. }), "{err}");
|
|
assert_eq!(host.calls().len(), 0);
|
|
|
|
let host = Arc::new(StubHost::new());
|
|
let mut plugin = load("test-plugin", "write", &["vault.write"], &["write"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}"));
|
|
plugin.run_command("write", "body").unwrap_or_else(|e| panic!("{e}"));
|
|
assert_eq!(host.calls(), vec![String::from("vault_write(notes/new.md,body)")]);
|
|
}
|
|
|
|
#[test]
|
|
fn back_to_back_calls_stay_inside_the_per_call_host_budget() {
|
|
// The budget is 8 host calls per millisecond *of one guest call*. Two calls in a row is
|
|
// 4 host calls; a plugin that is busy must not be charged for its neighbour's calls.
|
|
let host = Arc::new(StubHost::new());
|
|
let mut plugin = load("test-plugin", "good", &READ, &["echo"], host as Arc<_>).unwrap_or_else(|e| panic!("{e}"));
|
|
for round in 0..50 {
|
|
plugin.run_command("echo", "{}").unwrap_or_else(|e| panic!("round {round}: {e}"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_unknown_command_is_not_found() {
|
|
let host = Arc::new(StubHost::new());
|
|
let mut plugin = load("test-plugin", "good", &READ, &["echo"], host as Arc<_>).unwrap_or_else(|e| panic!("{e}"));
|
|
assert!(matches!(plugin.run_command("nope", "{}"), Err(Error::NotFound(_))));
|
|
}
|
|
|
|
#[test]
|
|
fn an_invalid_manifest_is_rejected_before_the_module_runs() {
|
|
let bad = Manifest { id: "Bad Id".into(), ..manifest_for("x", &[], &[]) };
|
|
let err = Plugin::load(bad, &common::wasm("good"), Arc::new(StubHost::new()), Limits::default()).unwrap_err();
|
|
assert!(matches!(err, Error::Manifest(_)), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn the_plugin_set_lists_and_dispatches_full_command_ids() {
|
|
let mut set = PluginSet::new();
|
|
set.insert(load("alpha", "good", &READ, &["echo", "shout"], Arc::new(StubHost::new())).unwrap_or_else(|e| panic!("{e}")));
|
|
set.insert(load("beta", "good", &READ, &["only"], Arc::new(StubHost::new())).unwrap_or_else(|e| panic!("{e}")));
|
|
assert_eq!(set.commands(), vec![String::from("plugin.alpha.echo"), String::from("plugin.alpha.shout"), String::from("plugin.beta.only")]);
|
|
assert_eq!(set.ids(), vec!["alpha", "beta"]);
|
|
assert!(set.get("alpha").is_some());
|
|
assert!(set.get_mut("alpha").is_some());
|
|
|
|
let out = set.dispatch("plugin.alpha.echo", r#"{"a":true}"#).unwrap_or_else(|e| panic!("{e}"));
|
|
assert_eq!(out, r#"{"a":true}"#);
|
|
assert!(matches!(set.dispatch("plugin.alpha.nope", "{}"), Err(Error::NotFound(_))));
|
|
assert!(matches!(set.dispatch("plugin.other.echo", "{}"), Err(Error::NotFound(_))));
|
|
assert!(matches!(set.dispatch("echo", "{}"), Err(Error::NotFound(_))));
|
|
assert!(set.remove("alpha"));
|
|
assert!(!set.remove("alpha"));
|
|
assert_eq!(set.len(), 1);
|
|
assert!(set.remove("beta"));
|
|
assert!(set.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn broadcast_reports_one_error_per_failing_plugin() {
|
|
let mut set = PluginSet::new();
|
|
set.insert(load("test-plugin", "trap", &[], &["echo"], Arc::new(StubHost::new())).unwrap_or_else(|e| panic!("{e}")));
|
|
let errors = set.broadcast_event(r#"{"type":"vault.indexed"}"#);
|
|
assert_eq!(errors.len(), 1);
|
|
assert_eq!(errors.first().map(|(id, _)| id.as_str()), Some("test-plugin"));
|
|
}
|