fix(rvagent-cli, ruqu-wasm): unblock 2 PR #388 test failures

PR #388's matrix-split CI exposed two pre-existing failures hidden
by the previous 30-minute Tests-job timeout. Both have surprising
root causes worth recording.

## Failure 1 — `rvagent-cli::a2a_cli::a2a_serve_discover_and_send_task`

Symptom: `unrecognized subcommand 'a2a'` from the spawned `rvagent`
binary; test panicked at the `expect(server closed before emitting
listening line)` site.

Root cause: **PR #380's `main.rs` and `Cargo.toml` changes were
silently lost during merge.** The new `crates/rvAgent/rvagent-cli/src/a2a.rs`
file landed, but:
  - `mod a2a;` was never added to `main.rs`
  - The `A2a(A2aCommand)` variant was never added to the `Commands`
    enum
  - The dispatch arm was never wired in
  - `Cargo.toml` was never updated with the new deps
    (`rvagent-a2a` path dep, `ed25519-dalek`, `rand_core`, `axum`,
    `reqwest`, `hex`, plus tokio's `signal`/`process`/`time`/`io-*`
    /`fs`/`net` features)

So `rvagent` shipped with `a2a.rs` orphaned: the file compiled into
the lib via `lib.rs` but the binary's `main.rs` never knew about it.

Fix:
  - `main.rs`: add `mod a2a;`, add `A2a(a2a::A2aCommand)` variant,
    add `is_tui_mode` arm, add dispatch arm using
    `cli.command.take()` to own the variant (avoids needing to
    derive Clone on every clap struct in `a2a.rs`).
  - `Cargo.toml`: restore the deps and tokio features PR #380
    intended.

Diagnostic improvement: also extended the test to drain the
server's stderr in the background and dump it on every panic
path. Without that I'd never have seen `unrecognized subcommand
'a2a'` — the future-me debugging this would have spent hours.

Verified locally: `cargo test -p rvagent-cli --test a2a_cli` →
`1 passed; 0 failed`.

## Failure 2 — `ruqu-wasm::tests::test_circuit_rejects_too_many_qubits`

Symptom: panic inside `wasm-bindgen-0.2.117/src/lib.rs:1280`
("function not implemented on non-wasm32 targets").

Root cause: the test module was `#[cfg(test)]` (runs on every
`cargo test`) but called into wasm-bindgen-wrapped types
(`WasmQuantumCircuit::new`), which since wasm-bindgen 0.2.117
panic when called from a non-wasm runtime.

Fix: gate the tests module on `#[cfg(all(test, target_arch =
"wasm32"))]`. WASM-binding tests run via `wasm-pack test`; the
underlying `ruqu-core` numeric logic is already covered by its
own native test suite.

This is the same pattern PR #390 (RaBitQ WASM) used proactively.

## Verification

  cargo build -p rvagent-cli                                 → clean
  cargo test  -p rvagent-cli --test a2a_cli                  → 1/1 pass
  cargo build -p ruqu-wasm                                   → clean
  cargo test  -p ruqu-wasm                                   → 0 native tests
                                                                (wasm-only path)
  cargo clippy -p rvagent-cli -p ruqu-wasm --all-targets
       --no-deps -- -D warnings                              → exit 0
  cargo fmt --all --check                                    → exit 0

After this lands, PR #388's Tests (rvagent) and Tests (ruqu-quantum)
shards should go green.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruvnet 2026-04-26 00:14:39 -04:00
parent ffba4d86d4
commit dd59745ed8
5 changed files with 71 additions and 9 deletions

6
Cargo.lock generated
View file

@ -10580,16 +10580,22 @@ dependencies = [
"anyhow",
"assert_cmd",
"async-trait",
"axum 0.8.8",
"chrono",
"clap",
"console",
"crossterm 0.28.1",
"dirs 5.0.1",
"dotenvy",
"ed25519-dalek",
"hex",
"indicatif",
"predicates",
"rand 0.8.5",
"rand_core 0.6.4",
"ratatui",
"reqwest 0.12.28",
"rvagent-a2a",
"rvagent-backends",
"rvagent-core",
"rvagent-middleware",

View file

@ -521,7 +521,10 @@ pub fn init() {
// Tests
// ═══════════════════════════════════════════════════════════════════════════
#[cfg(test)]
// Tests for the WASM bindings only run on wasm32 because wasm-bindgen
// 0.2.117 panics on `JsValue::from_str` from a non-wasm runtime.
// Native verification of the underlying logic lives in `ruqu-core`.
#[cfg(all(test, target_arch = "wasm32"))]
mod tests {
use super::*;

View file

@ -16,9 +16,10 @@ rvagent-backends = { path = "../rvagent-backends" }
rvagent-middleware = { path = "../rvagent-middleware" }
rvagent-tools = { path = "../rvagent-tools" }
rvagent-subagents = { path = "../rvagent-subagents" }
rvagent-a2a = { path = "../rvagent-a2a" }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["signal", "process", "time", "io-util", "io-std", "fs", "net"] }
thiserror = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
@ -34,7 +35,12 @@ ratatui = "0.29"
dirs = "5.0"
aes-gcm = "0.10"
rand = "0.8"
rand_core = "0.6"
dotenvy = "0.15"
ed25519-dalek = "2"
hex = "0.4"
axum = "0.8"
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
[dev-dependencies]
tempfile = "3.14"

View file

@ -4,6 +4,7 @@
//! initializes tracing, and dispatches to the appropriate run mode
//! (interactive TUI, single-prompt, or session management).
mod a2a;
mod app;
mod display;
mod mcp;
@ -60,6 +61,8 @@ enum Commands {
#[command(subcommand)]
action: SessionAction,
},
/// A2A (Agent-to-Agent) protocol operations: serve, discover, send-task.
A2a(a2a::A2aCommand),
}
// ---------------------------------------------------------------------------
@ -75,13 +78,14 @@ async fn main() -> Result<()> {
let _ = dotenvy::from_filename(".env.local");
}
let cli = Cli::parse();
let mut cli = Cli::parse();
// Determine if we're running in interactive TUI mode.
// In TUI mode, we suppress console tracing to avoid corrupting the display.
let is_tui_mode = match &cli.command {
Some(Commands::Session { .. }) => false,
Some(Commands::Run { .. }) => false,
Some(Commands::A2a(_)) => false,
Some(Commands::Chat) | None => cli.prompt.is_none(),
};
@ -112,6 +116,14 @@ async fn main() -> Result<()> {
app.run_once(prompt).await?;
}
// A2A protocol subcommands.
Some(Commands::A2a(_)) => {
// Take ownership so we don't need Clone on the inner clap types.
if let Some(Commands::A2a(cmd)) = cli.command.take() {
a2a::run(cmd).await?;
}
}
// Interactive TUI chat (default when no sub-command given).
Some(Commands::Chat) | None => {
// If --prompt is supplied without a sub-command, treat as non-interactive.

View file

@ -5,6 +5,7 @@
//! `discover` + `send-task` against it.
use std::process::Stdio;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, BufReader};
@ -39,17 +40,51 @@ async fn a2a_serve_discover_and_send_task() {
.expect("spawn rvagent a2a serve");
let stdout = server.stdout.take().expect("server stdout piped");
let stderr = server.stderr.take().expect("server stderr piped");
let mut reader = BufReader::new(stdout).lines();
// Drain stderr in the background so it doesn't block the child if the
// pipe fills, AND so we can dump it on diagnostic failure paths.
let stderr_buf: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
{
let buf = stderr_buf.clone();
tokio::spawn(async move {
use tokio::io::AsyncReadExt;
let mut reader = stderr;
let mut chunk = [0u8; 4096];
while let Ok(n) = reader.read(&mut chunk).await {
if n == 0 {
break;
}
buf.lock().unwrap().extend_from_slice(&chunk[..n]);
}
});
}
let dump_stderr = || -> String {
let raw = stderr_buf.lock().unwrap().clone();
String::from_utf8_lossy(&raw).into_owned()
};
// -- 2) Parse "listening on 127.0.0.1:<port>" from the first line.
//
// Give the server up to 20s to bind + print; CI under load is slower
// than local.
let line = tokio::time::timeout(Duration::from_secs(20), reader.next_line())
.await
.expect("server listening line timed out")
.expect("server stdout read error")
.expect("server closed before emitting listening line");
// than local. On every failure path we dump stderr so the actual
// error reason is visible.
let line = match tokio::time::timeout(Duration::from_secs(20), reader.next_line()).await {
Ok(Ok(Some(l))) => l,
Ok(Ok(None)) => panic!(
"server closed stdout before emitting listening line.\n--- server stderr ---\n{}",
dump_stderr()
),
Ok(Err(e)) => panic!(
"server stdout read error: {e}\n--- server stderr ---\n{}",
dump_stderr()
),
Err(_) => panic!(
"timed out waiting for server listening line (>20s)\n--- server stderr ---\n{}",
dump_stderr()
),
};
let addr = line
.strip_prefix("listening on ")
.unwrap_or_else(|| panic!("unexpected first-line stdout from server: {:?}", line))