Test and Iterate
Recipes for the part of plugin development that isn't writing the handler: getting a build into Mydia, firing events at it, reading what it did, and testing without a host at all. The tutorial walks the first loop end to end.
Install your build
Goal: put a freshly built component and its manifest into a development instance, approved and enabled.
-
Build the component:
The file is
target/wasm32-wasip2/release/<crate_name>.wasm, with hyphens in the crate name turned into underscores. -
From your Mydia checkout, install it with absolute paths:
--approvegrants the capabilities the manifest declares. Without it the plugin installs inactive, and you approve it in Admin > System > Plugins. Installing the same slug again replaces the bytes and manifest and clears the grant, so pass--approveeach time or approve it again in the UI. -
Restart the server so it loads the install:
The task starts its own copy of the app, so while the server is running it logs
a harmless Failed to bind iroh endpoint error. The install itself still
succeeds.
install refuses a bundled plugin's slug. To replace those bytes, see
Replace a bundled plugin's bytes.
Fire a test event
Goal: run your handler without waiting for real activity.
- Open Admin > System > Plugins and click Logs on your plugin's row.
- Open the Test tab.
- Pick an event from the list and click Run test.
The list holds the events the plugin subscribes to, so a plugin with no
events:subscribe entry has nothing to test, and a disabled plugin shows
"Enable this plugin to send it a test event." The handler gets a synthetic
event with invented detail (for media_item.added, the title is Test Movie),
and its log lines carry a test badge.
For a plugin with several instances, such as a setup-wizard source, the test runs against the first enabled instance.
Read logs and network requests
Goal: see what the handler logged and what requests it made.
Click Logs on the plugin's row. The modal has three tabs:
| Tab | Shows |
|---|---|
| Activity | The log lines from your guest, plus host lines for each invocation and its result. Filter by minimum level or search the text. |
| Network | Every outbound request the plugin made: time, method, URL, status, size, duration and outcome. Requests are gated by the net:http allowlist. |
| Test | The event picker from the previous recipe. |
Lines arrive live while the modal is open.
Test without a host
Goal: unit-test your handler logic with cargo test, no Wasm build and no
running Mydia.
Because the handler is plain Rust, you call it directly:
#[cfg(test)]
mod tests {
use super::*;
use mydia_plugin_sdk::types::Event;
fn event(kind: &str, metadata_json: &str) -> Event {
Event {
event: kind.into(),
category: None,
severity: None,
actor_type: None,
actor_id: None,
resource_type: None,
resource_id: None,
metadata_json: metadata_json.into(),
}
}
#[test]
fn handles_added() {
let evt = event("media_item.added", r#"{"metadata":{"title":"Example"}}"#);
assert!(on_event(evt).is_ok());
}
}
The host functions (http_request, data_read) only exist in the Wasm
component, so a test that builds for the native target cannot link them
directly. Keep your testable logic in plain functions, and wrap the host calls
behind a thin shim that is compiled out off-Wasm:
#[cfg(target_arch = "wasm32")]
fn send(req: &OutboundRequest) -> Option<mydia_plugin_sdk::types::OutboundResponse> {
mydia_plugin_sdk::host::http_request(req).ok()
}
#[cfg(not(target_arch = "wasm32"))]
fn send(_req: &OutboundRequest) -> Option<mydia_plugin_sdk::types::OutboundResponse> {
None // tests exercise the request-building logic, not the wire call
}
This is how the bundled notifier stays fully unit-tested. Its src/lib.rs is
worth reading for the pattern at scale.
Host logging in tests
The tutorial's handler calls host::log, which is also a host function. Put
it behind the same kind of shim if you want to unit-test that handler.
Swap bytes without reinstalling
Goal: rebuild and load new bytes without running install and restarting
every time.
Mydia reads an override directory as the highest-precedence source of plugin
bytes. A <slug>.wasm there shadows the installed copy, and the hyphenated or
underscored slug both work as the filename stem. The directory is read when the
server boots, so this needs one-time setup:
- Install the plugin once, as in Install your build.
- Set
PLUGINS_OVERRIDE_DIRto a directory you own in the environment the server starts in, then restart the server. -
Build and drop the bytes in with
sideload.sh, which lives in the Mydia checkout:It builds the crate and copies the component to
$PLUGINS_OVERRIDE_DIR/my-plugin.wasm. The same two steps by hand arecargo build --release --target wasm32-wasip2and acpof the built component to that path. -
In Admin > System > Plugins, click Disable, then Enable on the plugin. Enabling re-resolves the artifact, so the new bytes load without a restart.
- Run a test from Logs > Test.
The loop is edit, sideload.sh, Disable then Enable, test. The plugin must
already be installed so the host knows its capabilities; the override only
replaces the bytes. Changes to manifest.json still need a reinstall.
Replace a bundled plugin's bytes
Goal: run your own build of a plugin that ships with Mydia, such as to try a fix.
install refuses the slug of a bundled plugin. Use the override directory
instead: set PLUGINS_OVERRIDE_DIR before the server boots, place your build
there as <slug>.wasm, and restart. At boot the override shadows the bundled
bytes. Later builds go through the loop in
Swap bytes without reinstalling. Remove the
file and restart to return to the bundled copy.
Install on a real server
Goal: try an unpublished plugin on a running Mydia container.
Copy the component and its manifest.json somewhere the container can read,
such as the /config volume, then install them with mydia-cli:
Paths are resolved inside the container. This runs against the live server, so
the plugin starts without a restart. It installs inactive unless you add
--approve; approve its declared capabilities in Admin > System > Plugins
otherwise. Running the command again with a new build replaces the bytes and
manifest and clears the grant. A bundled plugin cannot be replaced this way; use
the override directory.