mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-06-01 14:39:33 +00:00
test(mmwave-bridge): production-ready CLI coverage + CI wiring (iter 118)
Iter 116 shipped the bridge → cluster integration with a manual live
test, but nothing committed. Production-ready means the integration
tests run on every commit. This iter closes the gap.
New tests/mmwave_bridge_cli.rs (7 tests, ~180 LOC):
bridge_simulator_emits_cycle_of_jsonl_events
spawns bridge --simulator --rate 10 for 700ms; asserts all four
frame kinds (breathing, heart_rate, distance, presence) appear in
stdout JSONL — guards against state-machine regressions that
would silently drop a frame type.
bridge_simulator_with_workers_posts_to_cluster
spawns fakeworker + bridge with --workers, asserts ≥3 successful
"posted text=" lines on stderr in 900ms and zero "cluster post
failed" lines. Verifies the iter-116 cluster sink path actually
composes with a live tonic server, not just unit-level mocks.
bridge_workers_without_fingerprint_refused_by_default
--workers + empty --fingerprint must fail before any RPC fires
(ADR-172 §2a parity with embed/bench). Guards against the gate
being bypassed in the bridge's discovery path.
bridge_workers_without_fingerprint_succeeds_with_opt_in
--allow-empty-fingerprint is the documented escape hatch for
legacy fleets; verify it actually works.
bridge_no_mode_flag_errors_cleanly
Running with no mode flag must produce a useful error referencing
the three valid mode flags. Operator-experience guard.
bridge_help_prints_synopsis
--help mentions --simulator, --workers, --fingerprint.
bridge_version_prints_pkg_name_and_version
--version output parses as `<name> <version>`.
CI changes (.github/workflows/hailo-backend-audit.yml):
- Path watcher now triggers on `crates/ruvector-mmwave/**` so a
regression in the shared parser fails CI before consumers
(firmware + bridge) can ship broken decoders.
- test job adds `cargo test --all-features` + clippy for the
standalone ruvector-mmwave crate. Tested independently so the
parser bisect cleanly when CI fails.
Validation:
- 17 test groups in the cluster crate now (was 16); 7 new bridge
tests join the matrix on default + tls feature configs.
- clippy --all-targets -D warnings clean for both ruvector-mmwave
(--all-features) and ruvector-hailo-cluster (default + tls).
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
2f331ad3a4
commit
99549cec2c
2 changed files with 214 additions and 0 deletions
11
.github/workflows/hailo-backend-audit.yml
vendored
11
.github/workflows/hailo-backend-audit.yml
vendored
|
|
@ -12,6 +12,7 @@ on:
|
|||
paths:
|
||||
- 'crates/ruvector-hailo-cluster/**'
|
||||
- 'crates/ruvector-hailo/**'
|
||||
- 'crates/ruvector-mmwave/**'
|
||||
- 'crates/hailort-sys/**'
|
||||
- 'docs/adr/ADR-167-*.md'
|
||||
- 'docs/adr/ADR-168-*.md'
|
||||
|
|
@ -26,6 +27,7 @@ on:
|
|||
paths:
|
||||
- 'crates/ruvector-hailo-cluster/**'
|
||||
- 'crates/ruvector-hailo/**'
|
||||
- 'crates/ruvector-mmwave/**'
|
||||
- 'crates/hailort-sys/**'
|
||||
- '.github/workflows/hailo-backend-audit.yml'
|
||||
|
||||
|
|
@ -111,6 +113,15 @@ jobs:
|
|||
- name: Run doctests
|
||||
working-directory: crates/ruvector-hailo-cluster
|
||||
run: cargo test --doc
|
||||
# Iter 115/118: shared mmwave parser crate. Tested independently
|
||||
# so a regression in the parser fails CI before it can corrupt
|
||||
# both the firmware and the host bridge that depend on it.
|
||||
- name: Test shared mmwave parser crate
|
||||
working-directory: crates/ruvector-mmwave
|
||||
run: cargo test --all-features
|
||||
- name: Clippy shared mmwave parser crate
|
||||
working-directory: crates/ruvector-mmwave
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
doc-warnings:
|
||||
name: missing-docs check
|
||||
|
|
|
|||
203
crates/ruvector-hailo-cluster/tests/mmwave_bridge_cli.rs
Normal file
203
crates/ruvector-hailo-cluster/tests/mmwave_bridge_cli.rs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
//! End-to-end CLI tests for the `ruvector-mmwave-bridge` binary
|
||||
//! (iter 118 — production-readiness pass).
|
||||
//!
|
||||
//! Verifies that the bridge actually composes with the cluster the way
|
||||
//! the manual live-test in iter 116 demonstrated, but committed and
|
||||
//! re-runnable in CI. Three cases:
|
||||
//!
|
||||
//! 1. `--simulator` mode without `--workers` produces the expected
|
||||
//! cycle of JSONL events on stdout.
|
||||
//! 2. `--simulator --workers` posts decoded events to a fakeworker
|
||||
//! via the embed RPC; assert successful posts on stderr.
|
||||
//! 3. `--workers` without `--fingerprint` is refused (ADR-172 §2a
|
||||
//! gate parity with embed/bench).
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
mod common;
|
||||
use common::{free_port, spawn_fakeworker};
|
||||
|
||||
const BRIDGE: &str = env!("CARGO_BIN_EXE_ruvector-mmwave-bridge");
|
||||
|
||||
#[test]
|
||||
fn bridge_simulator_emits_cycle_of_jsonl_events() {
|
||||
// 5 Hz × 1.5s = 7-8 events. Cycle is breathing → heart_rate →
|
||||
// distance → presence; assert at least one of each kind in the
|
||||
// window so a future state-machine bug that drops a frame type
|
||||
// surfaces.
|
||||
let mut child = Command::new(BRIDGE)
|
||||
.args(["--simulator", "--rate", "10", "--quiet"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("spawn bridge");
|
||||
|
||||
std::thread::sleep(Duration::from_millis(700));
|
||||
let _ = child.kill();
|
||||
let out = child.wait_with_output().expect("wait bridge");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let kinds: std::collections::HashSet<&str> = stdout
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
// Crude but sufficient: extract the "kind":"X" value.
|
||||
l.split("\"kind\":\"")
|
||||
.nth(1)
|
||||
.and_then(|s| s.split('"').next())
|
||||
})
|
||||
.collect();
|
||||
assert!(kinds.contains("breathing"), "no breathing event in {:?}", kinds);
|
||||
assert!(kinds.contains("heart_rate"), "no heart_rate event in {:?}", kinds);
|
||||
assert!(kinds.contains("distance"), "no distance event in {:?}", kinds);
|
||||
assert!(kinds.contains("presence"), "no presence event in {:?}", kinds);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_simulator_with_workers_posts_to_cluster() {
|
||||
let port = free_port();
|
||||
let mut worker = spawn_fakeworker(port, 4, "fp:bridge-test");
|
||||
|
||||
let mut child = Command::new(BRIDGE)
|
||||
.args([
|
||||
"--simulator",
|
||||
"--rate",
|
||||
"10",
|
||||
"--workers",
|
||||
&format!("127.0.0.1:{}", port),
|
||||
"--dim",
|
||||
"4",
|
||||
"--fingerprint",
|
||||
"fp:bridge-test",
|
||||
])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("spawn bridge with cluster sink");
|
||||
|
||||
// Let it pump for a moment — needs to dial the worker, send a few
|
||||
// RPCs, see results come back.
|
||||
std::thread::sleep(Duration::from_millis(900));
|
||||
let _ = child.kill();
|
||||
let out = child.wait_with_output().expect("wait bridge");
|
||||
let _ = worker.kill();
|
||||
let _ = worker.wait();
|
||||
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let post_count = stderr.matches("posted text=").count();
|
||||
assert!(
|
||||
post_count >= 3,
|
||||
"expected ≥ 3 cluster posts in window, saw {}: {}",
|
||||
post_count,
|
||||
stderr
|
||||
);
|
||||
// None of them should have failed — fakeworker is local, latency
|
||||
// budget is generous.
|
||||
assert!(
|
||||
!stderr.contains("cluster post failed"),
|
||||
"saw post failures: {}",
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_workers_without_fingerprint_refused_by_default() {
|
||||
// ADR-172 §2a parity: --workers + empty --fingerprint must fail
|
||||
// before any RPC is attempted, just like embed/bench.
|
||||
let out = Command::new(BRIDGE)
|
||||
.args([
|
||||
"--simulator",
|
||||
"--workers",
|
||||
"127.0.0.1:1", // never dialed; gate fires first
|
||||
"--dim",
|
||||
"4",
|
||||
// intentionally no --fingerprint
|
||||
])
|
||||
.output()
|
||||
.expect("run bridge");
|
||||
|
||||
assert!(!out.status.success(), "expected non-zero exit");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("ADR-172 §2a") || stderr.contains("empty --fingerprint"),
|
||||
"stderr should reference the §2a gate, got: {}",
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_workers_without_fingerprint_succeeds_with_opt_in() {
|
||||
let port = free_port();
|
||||
let mut worker = spawn_fakeworker(port, 4, ""); // fakeworker default fp
|
||||
|
||||
let mut child = Command::new(BRIDGE)
|
||||
.args([
|
||||
"--simulator",
|
||||
"--rate",
|
||||
"5",
|
||||
"--workers",
|
||||
&format!("127.0.0.1:{}", port),
|
||||
"--dim",
|
||||
"4",
|
||||
"--allow-empty-fingerprint",
|
||||
])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("spawn bridge");
|
||||
|
||||
std::thread::sleep(Duration::from_millis(900));
|
||||
let _ = child.kill();
|
||||
let out = child.wait_with_output().expect("wait bridge");
|
||||
let _ = worker.kill();
|
||||
let _ = worker.wait();
|
||||
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let post_count = stderr.matches("posted text=").count();
|
||||
assert!(
|
||||
post_count >= 1,
|
||||
"with --allow-empty-fingerprint, expected ≥ 1 post, saw {}: {}",
|
||||
post_count,
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_no_mode_flag_errors_cleanly() {
|
||||
let out = Command::new(BRIDGE)
|
||||
.output()
|
||||
.expect("run bridge with no args");
|
||||
assert!(!out.status.success(), "expected non-zero exit");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("--device") || stderr.contains("--simulator") || stderr.contains("--auto"),
|
||||
"error should name the missing mode flags, got: {}",
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_help_prints_synopsis() {
|
||||
let out = Command::new(BRIDGE)
|
||||
.arg("--help")
|
||||
.output()
|
||||
.expect("run bridge --help");
|
||||
assert!(out.status.success());
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(stdout.contains("--simulator"));
|
||||
assert!(stdout.contains("--workers"));
|
||||
assert!(stdout.contains("--fingerprint"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_version_prints_pkg_name_and_version() {
|
||||
let out = Command::new(BRIDGE)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.expect("run bridge --version");
|
||||
assert!(out.status.success());
|
||||
let line = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
assert_eq!(parts.len(), 2);
|
||||
assert_eq!(parts[0], "ruvector-hailo-cluster");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue