Implement credential-safe programs smoke gate (#97)
Some checks failed
Native code generation / deterministic (push) Failing after 2m12s
Imaging and meshing gate / native (push) Failing after 5m40s
JPEG 2000 feature / linux (push) Successful in 2m50s
Native Rust workspace compile / compile (push) Failing after 56s
Skia feature / linux (push) Successful in 31m24s

This commit is contained in:
2026-08-11 17:04:40 +00:00
parent 66f10baa1b
commit a9e2711447
18 changed files with 2216 additions and 25 deletions

View File

@@ -16,6 +16,8 @@ libremetaverse-voice-vivox = { path = "../crates/libremetaverse-voice-vivox" }
libremetaverse-voice-webrtc = { path = "../crates/libremetaverse-voice-webrtc" }
regex = "1.12"
roxmltree = "0.21.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1.47", features = ["io-std", "io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
[lints]
@@ -60,3 +62,7 @@ path = "src/bin/webrtc_test.rs"
[[bin]]
name = "osd-inspector"
path = "src/bin/osd_inspector.rs"
[[bin]]
name = "live-grid-smoke"
path = "src/bin/live_grid_smoke.rs"

View File

@@ -13,10 +13,37 @@ here so a source entry is never mistaken for a completed port.
| `prim-inspector` | PrimInspector | Implemented with live and deterministic fake-grid discovery |
| `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 |
| `test-client` | TestClient | All source commands implemented with live and deterministic fake-grid backends |
| `vivox-test` | VivoxTest | Implemented with gated live validation and a scripted fake TCP/control service |
| `webrtc-test` | WebRtcTest | Implemented with gated live WebRTC and a deterministic secure loopback peer |
## Final program completeness and live smoke
`live-grid-smoke --audit-only` compares the pinned source manifest with all
nine original native targets, their offline CLI suites, the generated
`TestClient` inventory, the implementation inventory, and the runtime command
registry. It succeeds only when both pending counts are zero.
The deterministic gate runs the same nine evidence stages without credentials
or network access:
```sh
cargo run -p libremetaverse-programs --bin live-grid-smoke -- \
--fake --evidence /tmp/metacrate-smoke-fake.jsonl
```
A live OpenSim/compatible-grid run reads the shared `GRID_USER="First Last"`,
`GRID_PASSWORD`, and `GRID_LOGIN_URL` values from the process environment or
workspace `.env`. The configured URI is used directly and the request includes
OpenSim-specific response options; Second Life voice caps are not required.
Credential transmission, one local-chat marker, a brief movement/return
teleport, and creation of a folder followed by a reversible move to Trash each
have their own literal opt-in. Existing evidence files are never overwritten.
The harness never spends L$, uploads assets, permanently deletes inventory,
changes estate or parcel state, or targets another user. See
[`docs/live-grid-smoke.md`](../docs/live-grid-smoke.md) for the exact command,
evidence schema, and dedicated-account precautions.
## WebRtcTest
`webrtc-test` uses the native `libremetaverse-voice-webrtc` adapter. Its

46
programs/build.rs Normal file
View File

@@ -0,0 +1,46 @@
use std::process::Command;
fn main() {
println!("cargo:rerun-if-env-changed=METACRATE_RUST_COMMIT");
println!("cargo:rerun-if-changed=../.git/HEAD");
println!("cargo:rerun-if-changed=../.git/packed-refs");
if let Some(reference) = git_reference() {
println!("cargo:rerun-if-changed=../.git/{reference}");
}
let commit = std::env::var("METACRATE_RUST_COMMIT")
.ok()
.filter(|value| valid_commit(value))
.or_else(git_commit)
.unwrap_or_else(|| "unavailable".to_owned());
println!("cargo:rustc-env=METACRATE_RUST_COMMIT={commit}");
}
fn git_reference() -> Option<String> {
let output = Command::new("git")
.args(["symbolic-ref", "--quiet", "HEAD"])
.output()
.ok()?;
let reference = String::from_utf8(output.stdout).ok()?;
let reference = reference.trim();
(output.status.success()
&& reference.starts_with("refs/")
&& reference
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"/-_.".contains(&byte)))
.then(|| reference.to_owned())
}
fn git_commit() -> Option<String> {
let output = Command::new("git")
.args(["rev-parse", "--verify", "HEAD"])
.output()
.ok()?;
let commit = String::from_utf8(output.stdout).ok()?;
let commit = commit.trim();
(output.status.success() && valid_commit(commit)).then(|| commit.to_owned())
}
fn valid_commit(value: &str) -> bool {
matches!(value.len(), 40 | 64) && value.bytes().all(|byte| byte.is_ascii_hexdigit())
}

View File

@@ -0,0 +1,3 @@
fn main() -> std::process::ExitCode {
libremetaverse_programs::live_grid_smoke::main_entry()
}

View File

@@ -0,0 +1,203 @@
//! Compile-time-backed audit of every pinned upstream program and `TestClient` command.
use crate::commands::TEST_CLIENT_COMMANDS;
use crate::test_client::{IMPLEMENTED_TEST_CLIENT_COMMANDS, registered_test_client_command_count};
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet};
const CARGO_MANIFEST: &str = include_str!("../Cargo.toml");
const SOURCE_MANIFEST: &str = include_str!("../upstream-programs.json");
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ProgramTarget {
pub project: &'static str,
pub binary: &'static str,
binary_source: &'static str,
offline_test_source: &'static str,
}
pub const ORIGINAL_PROGRAMS: &[ProgramTarget] = &[
ProgramTarget {
project: "OSDInspector",
binary: "osd-inspector",
binary_source: include_str!("bin/osd_inspector.rs"),
offline_test_source: include_str!("../tests/osd_inspector_cli.rs"),
},
ProgramTarget {
project: "SimpleBot",
binary: "simple-bot",
binary_source: include_str!("bin/simple_bot.rs"),
offline_test_source: include_str!("../tests/simple_bot_cli.rs"),
},
ProgramTarget {
project: "PacketDump",
binary: "packet-dump",
binary_source: include_str!("bin/packet_dump.rs"),
offline_test_source: include_str!("../tests/packet_dump_cli.rs"),
},
ProgramTarget {
project: "PrimInspector",
binary: "prim-inspector",
binary_source: include_str!("bin/prim_inspector.rs"),
offline_test_source: include_str!("../tests/prim_inspector_cli.rs"),
},
ProgramTarget {
project: "InventoryExplorer",
binary: "inventory-explorer",
binary_source: include_str!("bin/inventory_explorer.rs"),
offline_test_source: include_str!("../tests/inventory_explorer_cli.rs"),
},
ProgramTarget {
project: "IRCGateway",
binary: "irc-gateway",
binary_source: include_str!("bin/irc_gateway.rs"),
offline_test_source: include_str!("../tests/irc_gateway_cli.rs"),
},
ProgramTarget {
project: "TestClient",
binary: "test-client",
binary_source: include_str!("bin/test_client.rs"),
offline_test_source: include_str!("../tests/test_client_services_cli.rs"),
},
ProgramTarget {
project: "VivoxTest",
binary: "vivox-test",
binary_source: include_str!("bin/vivox_test.rs"),
offline_test_source: include_str!("../tests/vivox_test_cli.rs"),
},
ProgramTarget {
project: "WebRtcTest",
binary: "webrtc-test",
binary_source: include_str!("bin/webrtc_test.rs"),
offline_test_source: include_str!("../tests/webrtc_test_cli.rs"),
},
];
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CompletenessReport {
pub programs: usize,
pub source_files: usize,
pub test_client_commands: usize,
pub pending_programs: usize,
pub pending_commands: usize,
}
#[derive(Deserialize)]
struct SourceManifest {
upstream_commit: String,
projects: Vec<SourceProject>,
}
#[derive(Deserialize)]
struct SourceProject {
project: String,
files: Vec<SourceFile>,
}
#[derive(Deserialize)]
struct SourceFile {
path: String,
sha256: String,
}
/// Audits the pinned source manifest, all original Rust program targets, their
/// offline test entry points, and every generated `TestClient` command.
///
/// # Errors
///
/// Returns an error when the pinned source manifest is malformed or differs
/// from the native target, test, implementation, or runtime registries.
pub fn audit_program_completeness() -> Result<CompletenessReport, String> {
let manifest: SourceManifest = serde_json::from_str(SOURCE_MANIFEST)
.map_err(|_| "program source manifest is not valid JSON".to_owned())?;
if manifest.upstream_commit.len() != 40
|| !manifest
.upstream_commit
.bytes()
.all(|byte| byte.is_ascii_hexdigit())
{
return Err("program source manifest has an invalid upstream commit".into());
}
let mut projects = BTreeMap::new();
let mut source_paths = BTreeSet::new();
let mut source_files = 0_usize;
for project in &manifest.projects {
if project.files.is_empty() || projects.insert(project.project.as_str(), project).is_some()
{
return Err("program source manifest has an empty or duplicate project".into());
}
for file in &project.files {
if !source_paths.insert(file.path.as_str())
|| file.sha256.len() != 64
|| !file.sha256.bytes().all(|byte| byte.is_ascii_hexdigit())
{
return Err("program source manifest has an invalid file record".into());
}
source_files += 1;
}
}
let expected_projects: BTreeSet<_> = ORIGINAL_PROGRAMS
.iter()
.map(|target| target.project)
.collect();
if projects.keys().copied().collect::<BTreeSet<_>>() != expected_projects {
return Err("Rust program targets do not exactly match the source manifest".into());
}
for target in ORIGINAL_PROGRAMS {
let cargo_marker = format!("name = \"{}\"", target.binary);
if !CARGO_MANIFEST.contains(&cargo_marker)
|| target.binary_source.contains("pending_program")
|| !target.binary_source.contains("main_entry")
|| !target.offline_test_source.contains("#[test]")
{
return Err(format!(
"program {} lacks a native target or offline test",
target.project
));
}
}
let source_commands: BTreeSet<_> = projects["TestClient"]
.files
.iter()
.filter_map(|file| {
file.path
.contains("/Commands/")
.then(|| file.path.rsplit('/').next())
.flatten()
.and_then(|name| name.strip_suffix(".cs"))
})
.collect();
let generated_commands: BTreeSet<_> = TEST_CLIENT_COMMANDS.iter().copied().collect();
let implemented_commands: BTreeSet<_> =
IMPLEMENTED_TEST_CLIENT_COMMANDS.iter().copied().collect();
if source_commands != generated_commands
|| implemented_commands != generated_commands
|| registered_test_client_command_count() != generated_commands.len()
{
return Err("TestClient source, implementation, and runtime registries differ".into());
}
Ok(CompletenessReport {
programs: ORIGINAL_PROGRAMS.len(),
source_files,
test_client_commands: generated_commands.len(),
pending_programs: 0,
pending_commands: 0,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_source_program_and_command_has_a_native_tested_target() {
let report = audit_program_completeness().unwrap();
assert_eq!(report.programs, 9);
assert!(report.source_files > report.programs);
assert_eq!(report.test_client_commands, TEST_CLIENT_COMMANDS.len());
assert_eq!(report.pending_programs, 0);
assert_eq!(report.pending_commands, 0);
}
}

View File

@@ -1,8 +1,10 @@
//! Rust targets corresponding to the upstream example and tool projects.
pub mod commands;
pub mod completeness;
pub mod inventory_explorer;
pub mod irc_gateway;
pub mod live_grid_smoke;
pub mod osd_inspector;
pub mod packet_dump;
pub mod prim_inspector;

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
mod inventory;
mod services;
mod voice;
mod world;
use crate::commands::TEST_CLIENT_COMMANDS;
@@ -164,6 +165,8 @@ pub const IMPLEMENTED_TEST_CLIENT_COMMANDS: &[&str] = &[
"RegionInfoCommand",
"StatsCommand",
"UptimeCommand",
"ParcelVoiceInfo",
"VoiceAcountCommand",
];
#[must_use]
@@ -176,6 +179,11 @@ pub fn pending_test_client_commands() -> Vec<&'static str> {
.collect()
}
#[must_use]
pub fn registered_test_client_command_count() -> usize {
built_in_commands().len()
}
#[derive(Parser)]
#[command(
name = "test-client",
@@ -325,7 +333,9 @@ enum ClientEvent {
type BackendFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
type InventoryFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
trait ClientBackend: Send + Sync + inventory::Backend + services::Backend + world::Backend {
trait ClientBackend:
Send + Sync + inventory::Backend + services::Backend + voice::Backend + world::Backend
{
fn id(&self) -> UUID;
fn name(&self) -> String;
fn connected(&self) -> bool;
@@ -876,6 +886,7 @@ enum CommandHandler {
ImGroup,
Inventory(inventory::Command),
Services(services::Command),
Voice(voice::Command),
World(world::Command),
Load,
Login,
@@ -1038,6 +1049,7 @@ fn built_in_commands() -> HashMap<String, CommandDefinition> {
.chain(communication_commands())
.chain(inventory::commands())
.chain(services::commands())
.chain(voice::commands())
.chain(world::commands())
.map(|(name, description, category, handler)| {
(
@@ -1822,6 +1834,9 @@ async fn execute_client_command(
CommandHandler::Services(command) => {
services::execute(client.backend.as_ref(), *command, args, cancellation).await
}
CommandHandler::Voice(command) => {
voice::execute(client.backend.as_ref(), *command, args, cancellation).await
}
CommandHandler::World(command) => {
world::execute(client.backend.as_ref(), *command, args, cancellation).await
}
@@ -2716,7 +2731,7 @@ mod tests {
assert!(TEST_CLIENT_COMMANDS.contains(command));
assert!(!pending.contains(command));
}
assert_eq!(pending, ["ParcelVoiceInfo", "VoiceAcountCommand"]);
assert!(pending.is_empty());
}
#[test]

View File

@@ -0,0 +1,210 @@
//! Credential-safe native `TestClient` voice capability commands.
use super::{FakeBackend, InventoryFuture as BackendFuture, LiveBackend, lock};
use libremetaverse::types::compat::{CancellationToken, Uri};
use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser};
use std::collections::HashMap;
use std::time::Duration;
const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Copy)]
pub(super) enum Command {
Parcel,
Account,
}
pub(super) fn commands() -> [super::CommandEntry; 2] {
use super::{CommandCategory as Cat, CommandHandler};
[
(
"voiceparcel",
"Obtain redacted parcel voice information. Usage: voiceparcel",
Cat::Other,
CommandHandler::Voice(Command::Parcel),
),
(
"voiceaccount",
"Provision voice account information with credentials redacted. Usage: voiceaccount",
Cat::Other,
CommandHandler::Voice(Command::Account),
),
]
}
pub(super) trait Backend {
fn voice_query(
&self,
command: Command,
cancellation: CancellationToken,
) -> BackendFuture<'_, Result<VoiceResult, String>>;
}
pub(super) enum VoiceResult {
Parcel {
region_name_present: bool,
parcel_local_id: i32,
channel_present: bool,
},
Account {
username_present: bool,
password_present: bool,
},
}
pub(super) async fn execute<B: Backend + ?Sized>(
backend: &B,
command: Command,
args: &[String],
cancellation: CancellationToken,
) -> String {
if !args.is_empty() {
return match command {
Command::Parcel => "Usage: voiceparcel",
Command::Account => "Usage: voiceaccount",
}
.into();
}
match backend.voice_query(command, cancellation).await {
Ok(VoiceResult::Parcel {
region_name_present,
parcel_local_id,
channel_present,
}) => format!(
"Parcel voice info: region-name-present={region_name_present}, parcel-local-id={parcel_local_id}, channel=<{}>",
if channel_present {
"redacted"
} else {
"missing"
}
),
Ok(VoiceResult::Account {
username_present,
password_present,
}) => format!(
"Voice account provisioned: credentials=<{}>",
if username_present && password_present {
"redacted"
} else {
"incomplete"
}
),
Err(error) => error,
}
}
impl Backend for FakeBackend {
fn voice_query(
&self,
command: Command,
cancellation: CancellationToken,
) -> BackendFuture<'_, Result<VoiceResult, String>> {
Box::pin(async move {
if cancellation.is_cancellation_requested() {
return Err("Voice capability request cancelled".into());
}
Ok(match command {
Command::Parcel => {
self.record("CALL voice-parcel-info".into());
VoiceResult::Parcel {
region_name_present: true,
parcel_local_id: 42,
channel_present: true,
}
}
Command::Account => {
self.record("CALL voice-provision-account credentials=<redacted>".into());
VoiceResult::Account {
username_present: true,
password_present: true,
}
}
})
})
}
}
impl Backend for LiveBackend {
fn voice_query(
&self,
command: Command,
cancellation: CancellationToken,
) -> BackendFuture<'_, Result<VoiceResult, String>> {
Box::pin(async move {
let capability = match command {
Command::Parcel => "ParcelVoiceInfoRequest",
Command::Account => "ProvisionVoiceAccountRequest",
};
let simulator = self
.network
.current_sim()
.ok_or_else(|| "No current simulator is available".to_owned())?;
let uri = simulator
.native_capability_uri(capability)
.map_err(|_| "Voice capability lookup failed".to_owned())?
.ok_or_else(|| "Required voice capability is unavailable".to_owned())?;
let http = lock(&self.client).http_caps_client();
let request = http.post_with_uri_osd_format_osd_cancellation_token_i_progress(
Uri(uri.0),
OSDFormat::Xml,
OSD::Map(HashMap::new()),
cancellation.clone(),
None,
);
let result = tokio::select! {
() = cancellation.cancelled() => return Err("Voice capability request cancelled".into()),
() = tokio::time::sleep(REQUEST_TIMEOUT) => return Err("Voice capability request timed out".into()),
result = request => result,
}
.map_err(|_| "Voice capability request failed".to_owned())?;
let (response, bytes) = result;
if !response.is_success_status_code() || bytes.len() > MAX_RESPONSE_BYTES {
return Err("Voice capability returned an error".into());
}
let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)
.map_err(|_| "Voice capability returned malformed LLSD".to_owned())?
else {
return Err("Voice capability response was not an LLSD map".into());
};
Ok(match command {
Command::Parcel => {
let region_name_present = present_string(&map, "region_name");
let parcel_local_id = map
.get("parcel_local_id")
.and_then(|value| value.as_integer().ok())
.ok_or_else(|| "Parcel voice response omitted local ID".to_owned())?;
let channel_present = match map.get("voice_credentials") {
Some(OSD::Map(credentials)) => present_string(credentials, "channel_uri"),
_ => false,
};
if !region_name_present || !channel_present {
return Err("Parcel voice response omitted required fields".into());
}
VoiceResult::Parcel {
region_name_present,
parcel_local_id,
channel_present,
}
}
Command::Account => {
let username_present = present_string(&map, "username");
let password_present = present_string(&map, "password");
if !username_present || !password_present {
return Err("Voice account response omitted credentials".into());
}
VoiceResult::Account {
username_present,
password_present,
}
}
})
})
}
}
fn present_string(map: &HashMap<String, OSD>, key: &str) -> bool {
map.get(key)
.and_then(|value| value.as_string().ok())
.is_some_and(|value| !value.is_empty())
}

View File

@@ -0,0 +1,178 @@
use serde_json::Value;
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;
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
struct TestDir(PathBuf);
impl TestDir {
fn new() -> Self {
let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"metacrate-live-grid-smoke-{}-{id}",
std::process::id()
));
fs::create_dir(&path).expect("create smoke test directory");
Self(path)
}
fn path(&self, name: &str) -> PathBuf {
self.0.join(name)
}
}
impl Drop for TestDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn text(path: &Path) -> &str {
path.to_str().expect("UTF-8 test path")
}
fn run(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_live-grid-smoke"))
.args(args)
.env_remove("GRID_USER")
.env_remove("GRID_PASSWORD")
.env_remove("GRID_LOGIN_URL")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("run live-grid-smoke")
}
#[test]
fn help_documents_credentials_side_effect_gates_and_unsupported_dangerous_actions() {
let output = run(&["--help"]);
assert!(output.status.success());
let help = String::from_utf8(output.stdout).unwrap();
for marker in [
"GRID_USER",
"workspace .env",
"OpenSim/compatible-grid LLSD login endpoint",
"--fake",
"--audit-only",
"--evidence",
"--allow-live-login",
"--allow-public-chat",
"--allow-agent-movement",
"--allow-reversible-inventory",
"never spends L$",
"permanently deletes",
"targets another user",
] {
assert!(help.contains(marker), "help omitted {marker}:\n{help}");
}
assert_eq!(run(&[]).status.code(), Some(EXIT_USAGE));
}
#[test]
fn fake_smoke_records_every_sanitized_stage_without_skips_or_credentials() {
let directory = TestDir::new();
let evidence = directory.path("evidence.jsonl");
let output = run(&["--fake", "--evidence", text(&evidence)]);
let stdout = String::from_utf8(output.stdout).unwrap();
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(
output.status.success(),
"stdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(stderr.is_empty(), "{stderr}");
let contents = fs::read_to_string(evidence).unwrap();
assert!(!contents.contains("://"));
assert!(!contents.to_ascii_lowercase().contains("password"));
assert!(!contents.to_ascii_lowercase().contains("token"));
let records: Vec<Value> = contents
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(records.len(), 9);
let stages: Vec<_> = records
.iter()
.map(|record| record["stage"].as_str().unwrap())
.collect();
assert_eq!(
stages,
[
"completeness",
"login",
"capabilities-simulator",
"im-chat",
"movement-teleport",
"inventory-folder",
"object-properties",
"asset-texture",
"logout",
]
);
assert!(records.iter().all(|record| record["status"] == "ok"));
assert!(records.iter().all(|record| record["schema"] == 1));
assert!(
records
.iter()
.all(|record| record["recorded_unix_seconds"].as_u64().is_some())
);
assert!(
records
.iter()
.all(|record| record["program_version"].as_str().is_some())
);
assert!(
records
.iter()
.all(|record| record["rust_commit"].as_str().is_some())
);
assert_eq!(records[0]["metrics"]["programs"], 9);
assert_eq!(records[0]["metrics"]["pending_programs"], 0);
assert_eq!(records[0]["metrics"]["pending_commands"], 0);
assert_eq!(records[8]["metrics"]["active_tasks"], 0);
}
#[test]
fn audit_proves_all_programs_and_commands_without_credentials_or_evidence() {
let output = run(&["--audit-only"]);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(stdout.contains("programs=9"), "{stdout}");
assert!(stdout.contains("pending-programs=0"), "{stdout}");
assert!(stdout.contains("pending-commands=0"), "{stdout}");
}
#[test]
fn live_mode_requires_every_literal_confirmation_before_loading_credentials() {
let directory = TestDir::new();
let evidence = directory.path("evidence.jsonl");
let output = run(&[
"--evidence",
text(&evidence),
"--allow-live-login",
"--confirm-live-login",
"LOGIN",
]);
assert_eq!(output.status.code(), Some(EXIT_USAGE));
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("--allow-public-chat"), "{stderr}");
assert!(!evidence.exists());
}
#[test]
fn evidence_is_create_new_and_never_overwrites_a_previous_audit() {
let directory = TestDir::new();
let evidence = directory.path("evidence.jsonl");
fs::write(&evidence, "preserve-me\n").unwrap();
let output = run(&["--fake", "--evidence", text(&evidence)]);
assert_eq!(output.status.code(), Some(EXIT_INPUT));
assert_eq!(fs::read_to_string(evidence).unwrap(), "preserve-me\n");
}

View File

@@ -54,7 +54,7 @@ fn run(args: &[&str]) -> Output {
#[test]
#[allow(clippy::too_many_lines)]
fn full_offline_session_exercises_every_remaining_non_voice_command() {
fn full_offline_session_exercises_every_remaining_service_and_voice_command() {
let directory = TestDir::new();
let script = directory.write(&format!(
"!client\t{CLIENT}\tAlice\tBot\n\
@@ -106,6 +106,8 @@ fn full_offline_session_exercises_every_remaining_non_voice_command() {
invitegroup {AVATAR} {GROUP} {ROLE} --confirm\n\
joingroup Builders Guild --confirm\n\
leavegroup Builders Guild --confirm\n\
voiceparcel\n\
voiceaccount\n\
quit\n"
));
let output = run(&[
@@ -154,6 +156,8 @@ fn full_offline_session_exercises_every_remaining_non_voice_command() {
"invited",
"Joined the group Builders Guild",
"has left the group Builders Guild",
"Parcel voice info: region-name-present=true, parcel-local-id=42, channel=<redacted>",
"Voice account provisioned: credentials=<redacted>",
"CALL profile-clone",
"CALL generic-message",
"CALL animation-start",
@@ -162,6 +166,8 @@ fn full_offline_session_exercises_every_remaining_non_voice_command() {
"CALL group-invite",
"CALL group-join",
"CALL group-leave",
"CALL voice-parcel-info",
"CALL voice-provision-account credentials=<redacted>",
] {
assert!(stdout.contains(expected), "missing {expected:?}:\n{stdout}");
}
@@ -207,9 +213,6 @@ fn social_mutations_require_startup_and_per_command_confirmation() {
}
#[test]
fn command_inventory_leaves_only_explicit_voice_adapters() {
assert_eq!(
libremetaverse_programs::test_client::pending_test_client_commands(),
["ParcelVoiceInfo", "VoiceAcountCommand"]
);
fn command_inventory_has_no_pending_source_commands() {
assert!(libremetaverse_programs::test_client::pending_test_client_commands().is_empty());
}