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

This commit is contained in:
2026-08-11 14:09:51 +00:00
parent 21f85a1b58
commit da4afec708
16 changed files with 2462 additions and 21 deletions

View File

@@ -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]

View File

@@ -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

View File

@@ -1,3 +1,3 @@
fn main() -> std::process::ExitCode {
libremetaverse_programs::pending_program("VivoxTest")
libremetaverse_programs::vivox_test::main_entry()
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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"));
}