Implement native VivoxTest validation (#95)
Some checks failed
Native code generation / deterministic (push) Failing after 2m19s
Imaging and meshing gate / native (push) Failing after 4m24s
JPEG 2000 feature / linux (push) Successful in 2m50s
Native Rust workspace compile / compile (push) Failing after 15m31s
Skia feature / linux (push) Successful in 31m51s
Some checks failed
Native code generation / deterministic (push) Failing after 2m19s
Imaging and meshing gate / native (push) Failing after 4m24s
JPEG 2000 feature / linux (push) Successful in 2m50s
Native Rust workspace compile / compile (push) Failing after 15m31s
Skia feature / linux (push) Successful in 31m51s
This commit is contained in:
@@ -12,7 +12,9 @@ libremetaverse = { path = "../crates/libremetaverse", default-features = false }
|
||||
libremetaverse-imaging = { path = "../crates/libremetaverse-imaging", features = ["jpeg2000"] }
|
||||
libremetaverse-imaging-skia = { path = "../crates/libremetaverse-imaging-skia", features = ["skia"] }
|
||||
libremetaverse-structured-data = { path = "../crates/libremetaverse-structured-data" }
|
||||
libremetaverse-voice-vivox = { path = "../crates/libremetaverse-voice-vivox" }
|
||||
regex = "1.12"
|
||||
roxmltree = "0.21.1"
|
||||
tokio = { version = "1.47", features = ["io-std", "io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -14,9 +14,48 @@ here so a source entry is never mistaken for a completed port.
|
||||
| `inventory-explorer` | InventoryExplorer | Implemented with live inventory and deterministic AIS fixtures |
|
||||
| `irc-gateway` | IRCGateway | Implemented with live IRC/grid transports and deterministic offline scripts |
|
||||
| `test-client` | TestClient | All non-voice command groups implemented with live and deterministic fake-grid backends; voice adapters tracked by #95 and #96 |
|
||||
| `vivox-test` | VivoxTest | Pending milestone 11 issue #95 |
|
||||
| `vivox-test` | VivoxTest | Implemented with gated live validation and a scripted fake TCP/control service |
|
||||
| `webrtc-test` | WebRtcTest | Pending milestone 11 issue #96 |
|
||||
|
||||
## VivoxTest
|
||||
|
||||
`vivox-test` is a native async client of the Vivox SDK XML control protocol. It
|
||||
connects to an already-running service; it does not bundle, locate, start, or
|
||||
invoke the proprietary Vivox daemon, an SDK binary, a CLR, or the upstream C#
|
||||
program. Connector, provisional-account login, session, participant-volume,
|
||||
termination, account logout, connector shutdown, device enumeration, request
|
||||
correlation, and daemon events all use the public
|
||||
`libremetaverse-voice-vivox::VivoxControlClient` API.
|
||||
|
||||
Live validation is credential-safe and explicitly gated:
|
||||
|
||||
```text
|
||||
GRID_FIRST_NAME=... GRID_LAST_NAME=... GRID_PASSWORD=... \
|
||||
vivox-test --allow-live-login --confirm-live-login LOGIN
|
||||
```
|
||||
|
||||
The service endpoint defaults to `127.0.0.1:44124` and may be changed with
|
||||
`--daemon-endpoint IP:PORT`. The daemon and its proprietary SDK prerequisites
|
||||
must be installed and started separately. Capability URLs, provisioned account
|
||||
credentials, connector/account/session handles, and voice URIs never appear in
|
||||
diagnostics. Live parcel audio is a separate operation and is skipped unless
|
||||
`--allow-session-audio` is supplied.
|
||||
|
||||
Offline CI uses `--fake-script FILE`. The bounded, tab-separated file provides
|
||||
`capture-device`, `current-capture`, `render-device`, `current-render`,
|
||||
`provision`, `parcel`, and `participant` directives. An optional `reject`
|
||||
directive scripts a daemon failure. Fake mode binds an ephemeral IPv4 loopback
|
||||
port and performs the complete ten-request control flow over TCP before awaiting
|
||||
the service task and proving that no pipes, sessions, or tasks remain.
|
||||
|
||||
Run the issue-focused validation with:
|
||||
|
||||
```sh
|
||||
cargo test -p libremetaverse-voice-vivox
|
||||
cargo test -p libremetaverse-programs --test vivox_test_cli
|
||||
cargo test --manifest-path tests/compat/Cargo.toml --test vivox_protocol_semantics
|
||||
```
|
||||
|
||||
## OSDInspector
|
||||
|
||||
`osd-inspector` is a bounded, offline command-line client of the public native
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() -> std::process::ExitCode {
|
||||
libremetaverse_programs::pending_program("VivoxTest")
|
||||
libremetaverse_programs::vivox_test::main_entry()
|
||||
}
|
||||
|
||||
@@ -8,5 +8,6 @@ pub mod packet_dump;
|
||||
pub mod prim_inspector;
|
||||
pub mod simple_bot;
|
||||
pub mod test_client;
|
||||
pub mod vivox_test;
|
||||
|
||||
pub use libremetaverse::shim::pending_program;
|
||||
|
||||
1084
programs/src/vivox_test.rs
Normal file
1084
programs/src/vivox_test.rs
Normal file
File diff suppressed because it is too large
Load Diff
207
programs/tests/vivox_test_cli.rs
Normal file
207
programs/tests/vivox_test_cli.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
const EXIT_USAGE: i32 = 2;
|
||||
const EXIT_INPUT: i32 = 3;
|
||||
const EXIT_SERVICE: i32 = 4;
|
||||
|
||||
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct TestDir(PathBuf);
|
||||
|
||||
impl TestDir {
|
||||
fn new(name: &str) -> Self {
|
||||
let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"metacrate-vivox-test-{name}-{}-{id}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir(&path).expect("create test directory");
|
||||
Self(path)
|
||||
}
|
||||
|
||||
fn write(&self, contents: &str) -> PathBuf {
|
||||
let path = self.0.join("vivox.tsv");
|
||||
fs::write(&path, contents).expect("write fake Vivox script");
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn path_text(path: &Path) -> &str {
|
||||
path.to_str().expect("UTF-8 path")
|
||||
}
|
||||
|
||||
fn run(args: &[&str]) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_vivox-test"))
|
||||
.args(args)
|
||||
.env_remove("GRID_FIRST_NAME")
|
||||
.env_remove("GRID_LAST_NAME")
|
||||
.env_remove("GRID_PASSWORD")
|
||||
.env_remove("GRID_LOGIN_URL")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.expect("run vivox-test")
|
||||
}
|
||||
|
||||
fn utf8(bytes: &[u8]) -> &str {
|
||||
std::str::from_utf8(bytes).expect("UTF-8 process output")
|
||||
}
|
||||
|
||||
fn assert_exit(output: &Output, expected: i32) {
|
||||
assert_eq!(
|
||||
output.status.code(),
|
||||
Some(expected),
|
||||
"stdout:\n{}\nstderr:\n{}",
|
||||
utf8(&output.stdout),
|
||||
utf8(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn complete_script(extra: &str) -> String {
|
||||
format!(
|
||||
"capture-device\tFake Microphone\n\
|
||||
capture-device\tBackup Microphone\n\
|
||||
current-capture\tFake Microphone\n\
|
||||
render-device\tFake Speakers\n\
|
||||
current-render\tFake Speakers\n\
|
||||
provision\tvoice-user-secret\tfake-password-secret\tvoice.example.test\n\
|
||||
parcel\tScripted Region\t42\tsip:channel-token-secret@example.test\n\
|
||||
participant\tparticipant-secret\tAlice Resident\tsip:participant-token-secret@example.test\n\
|
||||
{extra}"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_documents_prerequisites_gates_and_scripted_controls() {
|
||||
let output = run(&["--help"]);
|
||||
assert!(output.status.success());
|
||||
let help = utf8(&output.stdout);
|
||||
for marker in [
|
||||
"[FIRSTNAME]",
|
||||
"[LASTNAME]",
|
||||
"[PASSWORD]",
|
||||
"--fake-script",
|
||||
"--daemon-endpoint",
|
||||
"--allow-live-login",
|
||||
"--confirm-live-login",
|
||||
"--allow-session-audio",
|
||||
"proprietary Vivox SDK control service",
|
||||
"does not bundle, locate, start, or invoke",
|
||||
"capture-device",
|
||||
"participant",
|
||||
] {
|
||||
assert!(help.contains(marker), "help omitted {marker}:\n{help}");
|
||||
}
|
||||
let output = run(&[]);
|
||||
assert_exit(&output, EXIT_USAGE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fake_tcp_service_exercises_full_flow_and_tears_down_without_secret_leaks() {
|
||||
let directory = TestDir::new("complete");
|
||||
let script = directory.write(&complete_script(""));
|
||||
let output = run(&[
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
"--timeout-seconds",
|
||||
"5",
|
||||
]);
|
||||
assert!(output.status.success(), "{}", utf8(&output.stderr));
|
||||
assert!(output.stderr.is_empty(), "{}", utf8(&output.stderr));
|
||||
let stdout = utf8(&output.stdout);
|
||||
for expected in [
|
||||
"* Fake Microphone",
|
||||
" Backup Microphone",
|
||||
"* Fake Speakers",
|
||||
"Voice connector created: <redacted>",
|
||||
"Provisioned voice account logged in: <redacted>",
|
||||
"region=Scripted Region local-id=42 channel=<redacted-uri>",
|
||||
"Voice session connected: <redacted>",
|
||||
"Participant control validated for Alice Resident (URI redacted).",
|
||||
"Voice session terminated.",
|
||||
"Voice account logged out.",
|
||||
"requests=10 active-pipes=0 active-sessions=0 active-tasks=0",
|
||||
] {
|
||||
assert!(stdout.contains(expected), "missing {expected}:\n{stdout}");
|
||||
}
|
||||
for secret in [
|
||||
"voice-user-secret",
|
||||
"fake-password-secret",
|
||||
"channel-token-secret",
|
||||
"participant-secret",
|
||||
"connector-secret",
|
||||
"account-secret",
|
||||
"session-secret",
|
||||
] {
|
||||
assert!(
|
||||
!stdout.contains(secret),
|
||||
"stdout leaked {secret}:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
!utf8(&output.stderr).contains(secret),
|
||||
"stderr leaked {secret}:\n{}",
|
||||
utf8(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejection_is_reported_by_codes_redacts_echoed_password_and_still_shuts_down() {
|
||||
let directory = TestDir::new("rejection");
|
||||
let script = directory.write(&complete_script(
|
||||
"reject\tAccount.Login.1\t1\t403\tdenied fake-password-secret\n",
|
||||
));
|
||||
let output = run(&["--fake-script", path_text(&script)]);
|
||||
assert_exit(&output, EXIT_SERVICE);
|
||||
let stderr = utf8(&output.stderr);
|
||||
assert!(stderr.contains("Account.Login.1"), "{stderr}");
|
||||
assert!(stderr.contains("return 1, status 403"), "{stderr}");
|
||||
assert!(stderr.contains("denied <redacted>"), "{stderr}");
|
||||
assert!(!stderr.contains("fake-password-secret"), "{stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_scripts_and_ungated_live_inputs_fail_before_network_access() {
|
||||
let directory = TestDir::new("invalid");
|
||||
let missing = directory.write("capture-device\tMicrophone\n");
|
||||
let output = run(&["--fake-script", path_text(&missing)]);
|
||||
assert_exit(&output, EXIT_INPUT);
|
||||
assert!(utf8(&output.stderr).contains("provision directive"));
|
||||
|
||||
let output = run(&["First", "Last", "do-not-echo"]);
|
||||
assert_exit(&output, EXIT_USAGE);
|
||||
assert!(utf8(&output.stderr).contains("--allow-live-login"));
|
||||
assert!(!utf8(&output.stderr).contains("do-not-echo"));
|
||||
|
||||
let output = run(&[
|
||||
"First",
|
||||
"Last",
|
||||
"do-not-echo",
|
||||
"--allow-live-login",
|
||||
"--confirm-live-login",
|
||||
"WRONG",
|
||||
]);
|
||||
assert_exit(&output, EXIT_USAGE);
|
||||
assert!(!utf8(&output.stderr).contains("do-not-echo"));
|
||||
|
||||
let valid = directory.write(&complete_script(""));
|
||||
let output = run(&[
|
||||
"First",
|
||||
"Last",
|
||||
"do-not-echo",
|
||||
"--fake-script",
|
||||
path_text(&valid),
|
||||
]);
|
||||
assert_exit(&output, EXIT_USAGE);
|
||||
assert!(!utf8(&output.stderr).contains("do-not-echo"));
|
||||
}
|
||||
Reference in New Issue
Block a user