Skip to content

test(plugin-wasm): add unit tests for conversions and wrapper#467

Open
staging-devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1779042323-cov-phase3-plugin-wasm
Open

test(plugin-wasm): add unit tests for conversions and wrapper#467
staging-devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1779042323-cov-phase3-plugin-wasm

Conversation

@staging-devin-ai-integration
Copy link
Copy Markdown
Contributor

@staging-devin-ai-integration staging-devin-ai-integration Bot commented May 17, 2026

Summary

Phase 3 coverage initiative — Stream B. Adds the first unit tests to crates/plugin-wasm (previously 0% — 0 tests across 3 files). 35 new #[test] blocks across #[cfg(test)] mod tests sections at the bottom of each target file, following the style anchor from #424 (table-driven where reasonable, serde_json::json! for JSON fixtures, no production-code changes).

Coverage breakdown:

  • crates/plugin-wasm/src/conversions.rs — every conversion variant:
    • TryFrom<wit_types::Packet> for streamkit_core::types::Packet: Audio, Text, Binary, Custom (valid JSON success path), Custom (invalid JSON error path with Invalid custom JSON: message).
    • From<streamkit_core::types::Packet> for wit_types::Packet: Audio, Text, Binary (drops content_type and metadata), Custom (JSON round-trip), Transcription (serialized to Binary JSON), Video (flattened to opaque Binary).
    • From<&wit_types::PacketType> for streamkit_core::types::PacketType: RawAudio (both Float32 and S16Le), OpusAudioEncodedAudio(Opus, codec_private=None), Text, Binary, Custom (preserves type_id), Any.
  • crates/plugin-wasm/src/wrapper.rs — only the pure-logic surfaces (no real wasmtime::component::Component involved):
    • serialize_params_to_json: None / Value::Null / non-null object / non-null primitive.
    • receive_from_any_input: empty input vec, pending packet with correct pin name, all-closed receivers drained out, closed-then-live ordering.
  • crates/plugin-wasm/src/lib.rs — only the parts that don't need a real .wasm artifact:
    • PluginRuntimeConfig::default values.
    • PluginRuntime::new with default and custom-memory-limit configs.
    • namespaced_kind branches (simple name → prefixed, already-prefixed idempotency, :: rejection, core:: reserved-prefix behavior).
    • PLUGIN_KIND_PREFIX constant.
    • load_plugins_from_directory on a missing directory and on a directory mixing non-.wasm files with a malformed .wasm file.

WasmNodeWrapper::input_pins / output_pins and the rest of run(), LoadedPlugin::create_node, and register_plugins are intentionally left for an integration-test follow-up — they all require instantiating a real WASM component, which is out of scope for this PR per the task spec.

Follow-ups / observations

Two tests pin currently-broken behavior with inline comments and do NOT fix it in this PR, per agent_docs/coverage.md §"When you spot a bug while writing tests". Both have tracking issues so the pins flip back to assertions of intended behavior when fixed:

  1. namespaced_kindcore:: reserved-prefix branch is unreachable (tracked in plugin-wasm: namespaced_kind core:: reserved-prefix branch is unreachable #470). The function checks kind.contains("::") before kind.starts_with("core::"), so any core::* input is rejected by the namespace-separator guard and never reaches the reserved-prefix check. tests::namespaced_kind_rejects_reserved_core_prefix pins this behavior.

  2. PluginRuntime::new rejects every enable_simd: false config (tracked in plugin-wasm: PluginRuntimeConfig.enable_simd=false fails initialization due to default relaxed-simd proposal #469). Wasmtime enables the relaxed-simd proposal by default, which requires the base SIMD proposal. As a result PluginRuntimeConfig { enable_simd: false, .. } always errors out with cannot disable the simd proposal but enable the relaxed simd proposal, regardless of enable_threads. tests::plugin_runtime_new_rejects_disabled_simd_due_to_relaxed_simd_default pins this behavior.

Review & Testing Checklist for Human

Notes

Link to Devin session: https://staging.itsdev.in/sessions/7223a199dcb3428ebd3fd9d6e431e9f7
Requested by: @streamer45


Devin Review

Status Commit
🕐 Outdated bb9a5d1 (HEAD is 8a8af40)

Run Devin Review

Open in Devin Review (Staging)

Cover previously-untested pure-logic surfaces of crates/plugin-wasm:

- conversions.rs: all wit_types <-> streamkit_core::types::Packet variants
  (Audio, Text, Binary, Custom valid + invalid JSON, Transcription/Video
  flattened to Binary) plus every PacketType mapping including the OpusAudio
  -> EncodedAudio(Opus, codec_private=None) translation.
- wrapper.rs: serialize_params_to_json (None / Null / object / primitive)
  and receive_from_any_input (empty / pending packet / all closed /
  closed-then-live).
- lib.rs: PluginRuntimeConfig defaults, runtime construction, namespaced_kind
  branches, PLUGIN_KIND_PREFIX constant, and load_plugins_from_directory
  behavior on a missing dir and a dir with mixed non-wasm/invalid-wasm files.

Two tests pin current broken behavior with inline comments and no fix
(per agent_docs/coverage.md):
- namespaced_kind: the 'core::' reserved-prefix branch is unreachable because
  the '::' separator guard catches it first.
- PluginRuntime::new: 'enable_simd: false' configurations always fail because
  wasmtime enables the relaxed-simd proposal by default.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Signed-off-by: Staging-Devin AI <166158716+staging-devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@staging-devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 8 potential issues.

Open in Devin Review (Staging)
Debug

Playground

Comment on lines +386 to +403
fn plugin_runtime_new_rejects_disabled_simd_due_to_relaxed_simd_default() {
// BUG: PluginRuntimeConfig exposes `enable_simd: false`, but wasmtime enables the
// relaxed-simd proposal by default, which requires the base SIMD proposal. The
// resulting config error ("cannot disable the simd proposal but enable the relaxed
// simd proposal") means every `enable_simd: false` config — including the threads
// combination — fails initialization. Either the field should be honored end-to-end
// (also disabling relaxed_simd) or removed from the public config. Pin current
// (broken) behavior until fixed.
for enable_threads in [false, true] {
let cfg = PluginRuntimeConfig {
max_memory_bytes: 16 * 1024 * 1024,
enable_simd: false,
enable_threads,
};
assert!(
PluginRuntime::new(cfg).is_err(),
"expected init to fail when enable_simd=false (enable_threads={enable_threads})"
);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Disabled-SIMD test locks in a public configuration bug

PluginRuntimeConfig exposes enable_simd, and PluginRuntime::new forwards it to Wasmtime via wasm_simd(config.enable_simd) without also disabling relaxed SIMD (crates/plugin-wasm/src/lib.rs:73-79). The new test now asserts that every enable_simd: false configuration must fail, even though its own comment calls that behavior broken, so CI will reject the correct runtime fix and the public config option remains unusable for callers that need SIMD disabled.

Prompt for agents
Fix the runtime contract instead of pinning the current failure. In crates/plugin-wasm/src/lib.rs, PluginRuntime::new should either fully honor PluginRuntimeConfig.enable_simd=false by configuring all dependent Wasmtime SIMD proposals consistently, or remove/replace the public option if disabling SIMD is intentionally unsupported. After the runtime behavior is corrected, update plugin_runtime_new_rejects_disabled_simd_due_to_relaxed_simd_default so it asserts the intended contract rather than expecting initialization failure.
Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledging the underlying bug is real — but this PR is scoped as test-only per agent_docs/coverage.md §"When you spot a bug while writing tests": pin current behavior + open a tracking issue, do not bundle a fix into a test-only PR. Filed as #469 with the repro and suggested fix. Inline comment now references the issue. When #469 lands, this test should be flipped to assert the intended contract.

}

#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Test module suppression lacks the required lint rationale

AGENTS.md requires every lint suppression to include an explanatory rationale (Linting discipline / Lint suppression rationales), but this new module-level #[allow(clippy::expect_used, clippy::unwrap_used)] has no // reason. That makes the PR violate the repository’s mandatory lint-suppression rule.

Suggested change
#[allow(clippy::expect_used, clippy::unwrap_used)]
#[allow(clippy::expect_used, clippy::unwrap_used)] // Tests use expect/unwrap to fail fast with readable assertion context.
Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8a8af40 — added a rationale comment above the #[allow].

}

#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 WASM runtime test suppression lacks the required lint rationale

AGENTS.md requires every lint suppression to include an explanatory rationale (Linting discipline / Lint suppression rationales), but this new module-level #[allow(clippy::expect_used, clippy::unwrap_used)] has no // reason. That makes the PR violate the repository’s mandatory lint-suppression rule.

Suggested change
#[allow(clippy::expect_used, clippy::unwrap_used)]
#[allow(clippy::expect_used, clippy::unwrap_used)] // Tests use expect/unwrap to fail fast with readable assertion context.
Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8a8af40 — added a rationale comment above the #[allow].

}

#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 WASM wrapper test suppression lacks the required lint rationale

AGENTS.md requires every lint suppression to include an explanatory rationale (Linting discipline / Lint suppression rationales), but this new module-level #[allow(clippy::expect_used, clippy::unwrap_used)] has no // reason. That makes the PR violate the repository’s mandatory lint-suppression rule.

Suggested change
#[allow(clippy::expect_used, clippy::unwrap_used)]
#[allow(clippy::expect_used, clippy::unwrap_used)] // Tests use expect/unwrap to fail fast with readable assertion context.
Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8a8af40 — added a rationale comment above the #[allow].

Comment thread crates/plugin-wasm/src/lib.rs Outdated
Comment on lines +449 to +450
// Ensure it doesn't exist
let _ = fs::remove_dir_all(&missing);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Temp-dir cleanup comment violates the repository comment rules

AGENTS.md explicitly forbids line-narration comments that just restate the next line. This new comment only says that the following remove_dir_all ensures the directory does not exist, so it violates the mandatory comment guideline and should be removed.

Suggested change
// Ensure it doesn't exist
let _ = fs::remove_dir_all(&missing);
let _ = fs::remove_dir_all(&missing);
Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8a8af40 — removed the line-narration comment.

Comment on lines +267 to +296
fn into_wit_transcription_packet_serializes_to_binary_json() {
let trans = TranscriptionData {
text: "hello world".to_string(),
segments: vec![],
language: Some("en".to_string()),
metadata: None,
};
let wit = wit_types::Packet::from(Packet::Transcription(Arc::new(trans)));
let wit_types::Packet::Binary(bytes) = wit else {
panic!("expected transcription to be flattened to Binary");
};
let parsed: serde_json::Value =
serde_json::from_slice(&bytes).expect("transcription JSON parses");
assert_eq!(parsed["text"], "hello world");
assert_eq!(parsed["language"], "en");
}

#[test]
fn into_wit_video_packet_flattens_to_binary_dropping_metadata() {
use streamkit_core::types::{PixelFormat, VideoFrame};
// Smallest valid Rgba8 frame: 1x1 = 4 bytes
let frame = VideoFrame::new(1, 1, PixelFormat::Rgba8, vec![0x11, 0x22, 0x33, 0x44])
.expect("valid frame");
let wit = wit_types::Packet::from(Packet::Video(frame));
match wit {
wit_types::Packet::Binary(bytes) => {
assert_eq!(bytes, vec![0x11, 0x22, 0x33, 0x44]);
},
other => panic!("expected video to flatten to Binary, got {other:?}"),
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Video/transcription flattening tests codify existing lossy WASM boundary behavior

The new conversion tests explicitly assert that Packet::Transcription and Packet::Video become WIT Binary packets. That matches the existing conversion implementation in crates/plugin-wasm/src/conversions.rs:61-95, including the one-time warning for video metadata loss, so I did not treat this as a PR-introduced behavior bug. The practical implication is that these tests will make the current lossy ABI behavior visible and will need updating when the WIT interface gains structured transcription/video types.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +426 to +436
fn namespaced_kind_rejects_reserved_core_prefix() {
let err = namespaced_kind("core::audio").expect_err("must reject `core::` kinds");
// The `core::` check is unreachable today because any string containing `::` is
// already rejected by the namespace-separator guard. Pin that behavior: the error
// message references the namespace-separator rule, not the reserved-prefix rule.
// If `namespaced_kind` is ever refactored to check the reserved prefix first,
// this assertion will start failing and signal that the contract should be revisited.
assert!(
err.contains("reserved for namespace prefixes"),
"expected namespace-separator error, got: {err}"
);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: The core-prefix validation branch remains intentionally unreachable

The namespaced_kind("core::audio") test asserts the namespace-separator error rather than the reserved-prefix error. This is consistent with the current guard order in crates/plugin-wasm/src/lib.rs:283-302, where any value containing :: is rejected before the core:: prefix check can run. I did not report this as a bug because the test comments make the pinned behavior explicit, but reviewers should be aware that the documented core::-specific error path is still not exercised by any input containing core::.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — the dead core:: branch is now tracked in #470. Inline BUG comment in 8a8af40 references the issue so the link is discoverable from the test source.

Comment on lines +445 to +459
#[tokio::test]
async fn receive_from_any_input_skips_closed_input_and_returns_from_live_input() {
let (tx_closed, rx_closed) = tokio::sync::mpsc::channel::<Packet>(1);
drop(tx_closed);
let (tx_live, rx_live) = tokio::sync::mpsc::channel::<Packet>(1);
tx_live.send(Packet::Text(Arc::from("from-live"))).await.expect("send succeeds");

let mut inputs = vec![("closed".to_string(), rx_closed), ("live".to_string(), rx_live)];

let (pin, packet) =
receive_from_any_input(&mut inputs).await.expect("live input must yield packet");
assert_eq!(pin, "live");
assert!(matches!(packet, Packet::Text(t) if t.as_ref() == "from-live"));
assert_eq!(inputs.len(), 1, "closed input must be removed");
assert_eq!(inputs[0].0, "live");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: receive_from_any_input preserves liveness after removing closed receivers

I checked whether swap_remove could make the helper skip or lose a still-open receiver. Because the function loops back after removing a closed receiver and re-polls the updated vector, the live receiver moved into the removed slot is still considered before returning; the new test covering a closed input before a live input exercises that behavior. This means the removal order change is not a packet-loss bug, although the helper still provides no fairness guarantee across always-ready inputs because it scans from index 0 each poll.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

- Add rationale comments to module-level clippy::expect_used /
  clippy::unwrap_used allows in conversions.rs, wrapper.rs, lib.rs
  (required by AGENTS.md 'Linting discipline').
- Remove line-narration comment in lib.rs.
- Reference tracking issues #469 (enable_simd=false bug) and #470
  (unreachable core:: branch in namespaced_kind) from the pinning
  tests' BUG comments.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Signed-off-by: Staging-Devin AI <166158716+staging-devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

Codecov Report

❌ Patch coverage is 94.94949% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.32%. Comparing base (fad6274) to head (8a8af40).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
crates/plugin-wasm/src/conversions.rs 91.51% 14 Missing ⚠️
crates/plugin-wasm/src/lib.rs 98.61% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #467      +/-   ##
==========================================
+ Coverage   65.91%   66.32%   +0.41%     
==========================================
  Files         217      217              
  Lines       57530    57827     +297     
  Branches     1597     1597              
==========================================
+ Hits        37922    38356     +434     
+ Misses      19602    19465     -137     
  Partials        6        6              
Flag Coverage Δ
backend 65.45% <94.94%> (+0.45%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
core 84.29% <ø> (ø)
engine 75.71% <ø> (ø)
api 84.73% <ø> (ø)
nodes 67.46% <ø> (+0.04%) ⬆️
server 57.16% <ø> (ø)
plugin-native 70.93% <ø> (ø)
plugin-wasm 74.11% <94.94%> (+67.74%) ⬆️
ui-services 74.73% <ø> (ø)
ui-components 60.49% <ø> (ø)
Files with missing lines Coverage Δ
crates/plugin-wasm/src/wrapper.rs 67.17% <100.00%> (+67.17%) ⬆️
crates/plugin-wasm/src/lib.rs 57.26% <98.61%> (+45.01%) ⬆️
crates/plugin-wasm/src/conversions.rs 94.09% <91.51%> (+94.09%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant