Implement native TestClient shell
Some checks failed
Native Rust workspace compile / compile (push) Failing after 12m49s
Some checks failed
Native Rust workspace compile / compile (push) Failing after 12m49s
This commit is contained in:
@@ -9,7 +9,7 @@ publish = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
libremetaverse = { path = "../crates/libremetaverse", default-features = false }
|
libremetaverse = { path = "../crates/libremetaverse", default-features = false }
|
||||||
tokio = { version = "1.47", features = ["io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
tokio = { version = "1.47", features = ["io-std", "io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ here so a source entry is never mistaken for a completed port.
|
|||||||
| `prim-inspector` | PrimInspector | Implemented with live and deterministic fake-grid discovery |
|
| `prim-inspector` | PrimInspector | Implemented with live and deterministic fake-grid discovery |
|
||||||
| `inventory-explorer` | InventoryExplorer | Implemented with live inventory and deterministic AIS fixtures |
|
| `inventory-explorer` | InventoryExplorer | Implemented with live inventory and deterministic AIS fixtures |
|
||||||
| `irc-gateway` | IRCGateway | Implemented with live IRC/grid transports and deterministic offline scripts |
|
| `irc-gateway` | IRCGateway | Implemented with live IRC/grid transports and deterministic offline scripts |
|
||||||
| `test-client` | TestClient | Pending milestone 11 issues #91–#94 |
|
| `test-client` | TestClient | Native shell, registry, system, and communication groups implemented; remaining command groups tracked by #92–#94 |
|
||||||
| `vivox-test` | VivoxTest | Pending milestone 11 issue #95 |
|
| `vivox-test` | VivoxTest | Pending milestone 11 issue #95 |
|
||||||
| `webrtc-test` | WebRtcTest | Pending milestone 11 issue #96 |
|
| `webrtc-test` | WebRtcTest | Pending milestone 11 issue #96 |
|
||||||
|
|
||||||
@@ -151,6 +151,64 @@ cargo test -p libremetaverse-programs irc_gateway::tests --locked
|
|||||||
cargo test --manifest-path tests/compat/Cargo.toml --test social_message_semantics --locked
|
cargo test --manifest-path tests/compat/Cargo.toml --test social_message_semantics --locked
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## TestClient
|
||||||
|
|
||||||
|
`test-client` is a native asynchronous multi-avatar command shell. It accepts
|
||||||
|
one account with `--first`, `--last`, and `--pass`, or a bounded account file
|
||||||
|
with `--file`. Account-file records use `First Last Password` followed by an
|
||||||
|
optional `Region/x/y/z` start location. `--loginuri`, `--startpos`, `--master`,
|
||||||
|
`--masterkey`, `--groupcommands`, `--scriptfile`, and `--nogui` preserve the
|
||||||
|
upstream shell controls. Each login has a configurable timeout, the registry is
|
||||||
|
limited to 64 clients, and `@ First Last` selects one client while `@` restores
|
||||||
|
broadcast command routing.
|
||||||
|
|
||||||
|
The implemented command groups are `@`, `debug`, `echomaster`, `help`, `im`,
|
||||||
|
`imgroup`, `load`, `login`, `logpacket`, `logout`, `md5`, `quit`, `say`,
|
||||||
|
`setmaster`, `setmasterkey`, `shout`, `showeffects`, `sleep`, `waitforlogin`, and
|
||||||
|
`whisper`. Chat and instant-message bodies are bounded to the grid protocol
|
||||||
|
limit. Group commands require both `--groupcommands` and current group
|
||||||
|
membership. Master chat can be echoed, master teleport lures are accepted, and
|
||||||
|
remote `login` and `md5` command text is redacted from transcripts. Packet logs
|
||||||
|
contain only timestamped packet type, simulator name, and byte count; they are
|
||||||
|
limited to 10,000 records and 16 MiB.
|
||||||
|
|
||||||
|
The native `load` command reads a portable command-alias manifest instead of a
|
||||||
|
CLR assembly. Each non-comment line is tab-separated
|
||||||
|
`name<TAB>description<TAB>template`; `{args}` or `$*` inserts the quoted command
|
||||||
|
arguments. Manifests are limited to 1 MiB and 128 commands, with an alias
|
||||||
|
expansion depth of eight.
|
||||||
|
|
||||||
|
Use `--fake-script FILE` for deterministic, credential-free terminal and grid
|
||||||
|
validation. Normal lines are dispatched exactly like interactive input. Fake
|
||||||
|
grid records begin with `!` and use tab-separated fields:
|
||||||
|
|
||||||
|
```text
|
||||||
|
!client<TAB>client-uuid<TAB>first<TAB>last
|
||||||
|
!client<TAB>client-uuid<TAB>first<TAB>last<TAB>master-name<TAB>master-uuid<TAB>true|false
|
||||||
|
!person<TAB>avatar-name<TAB>avatar-uuid
|
||||||
|
!group-member<TAB>avatar-uuid
|
||||||
|
!chat<TAB>client-uuid<TAB>source-uuid<TAB>name<TAB>message
|
||||||
|
!im<TAB>client-uuid<TAB>source-uuid<TAB>name<TAB>agent|object|teleport<TAB>true|false<TAB>message<TAB>session-uuid
|
||||||
|
!packet<TAB>client-uuid<TAB>packet-type<TAB>simulator<TAB>bytes
|
||||||
|
!effect<TAB>client-uuid<TAB>summary
|
||||||
|
!disconnect<TAB>client-uuid<TAB>reason
|
||||||
|
!cancel
|
||||||
|
!shutdown
|
||||||
|
```
|
||||||
|
|
||||||
|
Inputs are limited to 1 MiB and 4,096 lines. The fake transcript records native
|
||||||
|
backend calls and ends with connected-client, pending-login, active-task,
|
||||||
|
shutdown, dropped-event, and pending-command-inventory state. Run the focused
|
||||||
|
framework and compatibility checks with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test -p libremetaverse-programs --test test_client_cli --locked
|
||||||
|
cargo test -p libremetaverse-programs test_client::tests --locked
|
||||||
|
cargo test --manifest-path tests/compat/Cargo.toml --test core_runtime_shims --locked
|
||||||
|
cargo test --manifest-path tests/compat/Cargo.toml --test social_message_semantics --locked
|
||||||
|
cargo test --manifest-path tests/compat/Cargo.toml --test network_semantics --locked
|
||||||
|
```
|
||||||
|
|
||||||
## PacketDump
|
## PacketDump
|
||||||
|
|
||||||
`packet-dump` preserves the upstream live arguments and its 20-second login
|
`packet-dump` preserves the upstream live arguments and its 20-second login
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
fn main() -> std::process::ExitCode {
|
fn main() -> std::process::ExitCode {
|
||||||
let command_count = libremetaverse_programs::commands::TEST_CLIENT_COMMANDS.len();
|
libremetaverse_programs::test_client::main_entry()
|
||||||
eprintln!("TestClient command inventory contains {command_count} translated targets");
|
|
||||||
libremetaverse_programs::pending_program("TestClient")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ pub mod osd_inspector;
|
|||||||
pub mod packet_dump;
|
pub mod packet_dump;
|
||||||
pub mod prim_inspector;
|
pub mod prim_inspector;
|
||||||
pub mod simple_bot;
|
pub mod simple_bot;
|
||||||
|
pub mod test_client;
|
||||||
|
|
||||||
pub use libremetaverse::shim::pending_program;
|
pub use libremetaverse::shim::pending_program;
|
||||||
|
|||||||
2502
programs/src/test_client.rs
Normal file
2502
programs/src/test_client.rs
Normal file
File diff suppressed because it is too large
Load Diff
273
programs/tests/test_client_cli.rs
Normal file
273
programs/tests/test_client_cli.rs
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, Output, Stdio};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
const ALICE: &str = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
||||||
|
const BOB: &str = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff";
|
||||||
|
const MASTER: &str = "11111111-2222-3333-4444-555555555555";
|
||||||
|
const TARGET: &str = "22222222-3333-4444-5555-666666666666";
|
||||||
|
const GROUP: &str = "33333333-4444-5555-6666-777777777777";
|
||||||
|
const GROUP_MEMBER: &str = "44444444-5555-6666-7777-888888888888";
|
||||||
|
const STRANGER: &str = "55555555-6666-7777-8888-999999999999";
|
||||||
|
const SESSION: &str = "66666666-7777-8888-9999-aaaaaaaaaaaa";
|
||||||
|
const EXIT_USAGE: i32 = 2;
|
||||||
|
const EXIT_INPUT: i32 = 3;
|
||||||
|
|
||||||
|
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-test-client-{name}-{}-{id}",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
fs::create_dir(&path).expect("create test directory");
|
||||||
|
Self(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&self, name: &str, contents: &str) -> PathBuf {
|
||||||
|
let path = self.0.join(name);
|
||||||
|
fs::write(&path, contents).expect("write fixture");
|
||||||
|
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 path_text(path: &Path) -> &str {
|
||||||
|
path.to_str().expect("UTF-8 test path")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(args: &[&str]) -> Output {
|
||||||
|
Command::new(env!("CARGO_BIN_EXE_test-client"))
|
||||||
|
.args(args)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()
|
||||||
|
.expect("run test-client")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stdout(output: &Output) -> &str {
|
||||||
|
std::str::from_utf8(&output.stdout).expect("UTF-8 stdout")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stderr(output: &Output) -> &str {
|
||||||
|
std::str::from_utf8(&output.stderr).expect("UTF-8 stderr")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_exit(output: &Output, expected: i32) {
|
||||||
|
assert_eq!(
|
||||||
|
output.status.code(),
|
||||||
|
Some(expected),
|
||||||
|
"stdout:\n{}\nstderr:\n{}",
|
||||||
|
stdout(output),
|
||||||
|
stderr(output)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_documents_upstream_framework_and_owned_commands() {
|
||||||
|
let output = run(&["--help"]);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let text = stdout(&output);
|
||||||
|
for marker in [
|
||||||
|
"--first",
|
||||||
|
"--last",
|
||||||
|
"--pass",
|
||||||
|
"--file",
|
||||||
|
"--loginuri",
|
||||||
|
"--startpos",
|
||||||
|
"--master",
|
||||||
|
"--masterkey",
|
||||||
|
"--groupcommands",
|
||||||
|
"--gettextures",
|
||||||
|
"--scriptfile",
|
||||||
|
"--nogui",
|
||||||
|
"--fake-script",
|
||||||
|
"echomaster",
|
||||||
|
"imgroup",
|
||||||
|
"logpacket",
|
||||||
|
"waitforlogin",
|
||||||
|
] {
|
||||||
|
assert!(text.contains(marker), "help omitted {marker}:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = run(&["--first", "Only"]);
|
||||||
|
assert_exit(&output, EXIT_USAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scripted_terminal_exercises_registry_communication_system_and_remote_auth() {
|
||||||
|
let directory = TestDir::new("commands");
|
||||||
|
let packet_log = directory.path("packets.log");
|
||||||
|
let script = directory.write(
|
||||||
|
"terminal.tsv",
|
||||||
|
&format!(
|
||||||
|
"!client\t{ALICE}\tAlice\tBot\tMaster Resident\t{MASTER}\ttrue\n\
|
||||||
|
!client\t{BOB}\tBob\tBot\tMaster Resident\t{MASTER}\ttrue\n\
|
||||||
|
!person\tTarget Resident\t{TARGET}\n\
|
||||||
|
!group-member\t{GROUP_MEMBER}\n\
|
||||||
|
help say\n\
|
||||||
|
say 7 \"hello world\"\n\
|
||||||
|
whisper quiet\n\
|
||||||
|
shout loud\n\
|
||||||
|
@ Alice Bot\n\
|
||||||
|
im Target Resident private hello\n\
|
||||||
|
imgroup {GROUP} group hello\n\
|
||||||
|
echomaster\n\
|
||||||
|
showeffects on\n\
|
||||||
|
logpacket 2 \"{}\"\n\
|
||||||
|
!chat\t{ALICE}\t{MASTER}\tMaster Resident\techo this\n\
|
||||||
|
!effect\t{ALICE}\tViewerEffect [LookAt] source={MASTER}\n\
|
||||||
|
!packet\t{ALICE}\tChatFromSimulator\tFake Region\t120\n\
|
||||||
|
!packet\t{ALICE}\tImprovedInstantMessage\tFake Region\t240\n\
|
||||||
|
!im\t{ALICE}\t{MASTER}\tMaster Resident\tagent\tfalse\tsay remote works\t{SESSION}\n\
|
||||||
|
!im\t{ALICE}\t{GROUP_MEMBER}\tGroup Member\tagent\ttrue\twhisper group works\t{SESSION}\n\
|
||||||
|
!im\t{ALICE}\t{STRANGER}\tStranger Resident\tagent\tfalse\tlogin Victim Resident supersecret\t{SESSION}\n\
|
||||||
|
setmaster Target Resident\n\
|
||||||
|
setmasterkey {MASTER}\n\
|
||||||
|
!im\t{ALICE}\t{MASTER}\tMaster Resident\tteleport\tfalse\tlure\t{SESSION}\n\
|
||||||
|
sleep 0\n\
|
||||||
|
debug info\n\
|
||||||
|
md5 secret\n\
|
||||||
|
logout\n\
|
||||||
|
@\n\
|
||||||
|
say after logout\n\
|
||||||
|
quit\n",
|
||||||
|
path_text(&packet_log)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let output = run(&["--fake-script", path_text(&script)]);
|
||||||
|
assert!(output.status.success(), "{}", stderr(&output));
|
||||||
|
assert!(output.stderr.is_empty(), "{}", stderr(&output));
|
||||||
|
let text = stdout(&output);
|
||||||
|
for expected in [
|
||||||
|
"Say something. Usage: say [optional-channel] message",
|
||||||
|
"[Alice Bot] CALL chat channel=7 type=Normal hello world",
|
||||||
|
"[Bob Bot] CALL chat channel=7 type=Normal hello world",
|
||||||
|
"[Alice Bot] CALL instant-message 22222222-3333-4444-5555-666666666666 private hello",
|
||||||
|
"[Alice Bot] CALL group-chat-join 33333333-4444-5555-6666-777777777777",
|
||||||
|
"[Alice Bot] CALL group-instant-message 33333333-4444-5555-6666-777777777777 group hello",
|
||||||
|
"[Alice Bot] CALL chat channel=0 type=Normal echo this",
|
||||||
|
"ViewerEffect [LookAt] source=11111111-2222-3333-4444-555555555555",
|
||||||
|
"[Alice Bot] CALL chat channel=0 type=Normal remote works",
|
||||||
|
"[Alice Bot] CALL chat channel=0 type=Whisper group works",
|
||||||
|
"<sensitive command redacted>",
|
||||||
|
"Master set to Target Resident",
|
||||||
|
"CALL teleport-lure-respond",
|
||||||
|
"CALL agent-pause",
|
||||||
|
"CALL agent-resume",
|
||||||
|
"Logging is set to Info",
|
||||||
|
"$1$5ebe2294ecd0e0f08eab7690d2a6ee69",
|
||||||
|
"[Bob Bot] CALL chat channel=0 type=Normal after logout",
|
||||||
|
"active-tasks=0 shutdown=true",
|
||||||
|
] {
|
||||||
|
assert!(text.contains(expected), "missing {expected}:\n{text}");
|
||||||
|
}
|
||||||
|
assert!(!text.contains("supersecret"), "credential leaked:\n{text}");
|
||||||
|
assert_eq!(text.matches("CALL logout-dispose").count(), 2, "{text}");
|
||||||
|
let packet_text = fs::read_to_string(packet_log).expect("read packet log");
|
||||||
|
assert_eq!(packet_text.lines().count(), 2, "{packet_text}");
|
||||||
|
assert!(packet_text.contains("ChatFromSimulator"));
|
||||||
|
assert!(packet_text.contains("ImprovedInstantMessage"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn login_replaces_duplicate_client_and_waitforlogin_is_deterministic() {
|
||||||
|
let directory = TestDir::new("login");
|
||||||
|
let script = directory.write(
|
||||||
|
"terminal.tsv",
|
||||||
|
"login Carol Bot credential-one\n\
|
||||||
|
waitforlogin\n\
|
||||||
|
login Carol Bot credential-two\n\
|
||||||
|
say only replacement\n\
|
||||||
|
quit\n",
|
||||||
|
);
|
||||||
|
let output = run(&["--fake-script", path_text(&script)]);
|
||||||
|
assert!(output.status.success(), "{}", stderr(&output));
|
||||||
|
let text = stdout(&output);
|
||||||
|
assert!(text.contains("currently tracking 1 bots"), "{text}");
|
||||||
|
assert_eq!(text.matches("Logged in Carol Bot").count(), 2, "{text}");
|
||||||
|
assert_eq!(
|
||||||
|
text.matches("CALL chat channel=0 type=Normal only replacement")
|
||||||
|
.count(),
|
||||||
|
1,
|
||||||
|
"{text}"
|
||||||
|
);
|
||||||
|
assert!(!text.contains("credential-one"), "password leaked:\n{text}");
|
||||||
|
assert!(!text.contains("credential-two"), "password leaked:\n{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_alias_manifest_and_parser_are_bounded_and_deterministic() {
|
||||||
|
let directory = TestDir::new("load");
|
||||||
|
let manifest = directory.write(
|
||||||
|
"commands.tsv",
|
||||||
|
"greet\tSend a greeting through channel 9\tsay 9 hello {args}\n",
|
||||||
|
);
|
||||||
|
let script = directory.write(
|
||||||
|
"terminal.tsv",
|
||||||
|
&format!(
|
||||||
|
"!client\t{ALICE}\tAlice\tBot\n\
|
||||||
|
load \"{}\"\n\
|
||||||
|
help greet\n\
|
||||||
|
greet \"quoted world\"\n\
|
||||||
|
say 'unterminated\n\
|
||||||
|
quit\n",
|
||||||
|
path_text(&manifest)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let output = run(&["--fake-script", path_text(&script)]);
|
||||||
|
assert!(output.status.success(), "{}", stderr(&output));
|
||||||
|
let text = stdout(&output);
|
||||||
|
assert!(text.contains("Loaded 1 native command alias"), "{text}");
|
||||||
|
assert!(text.contains("Send a greeting through channel 9"), "{text}");
|
||||||
|
assert!(
|
||||||
|
text.contains("CALL chat channel=9 type=Normal hello quoted world"),
|
||||||
|
"{text}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.contains("Command parse error: unterminated quoted token"),
|
||||||
|
"{text}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_inputs_and_cancellation_fail_or_shutdown_cleanly() {
|
||||||
|
let directory = TestDir::new("invalid");
|
||||||
|
let invalid = directory.write("terminal.tsv", "!unknown\tvalue\n");
|
||||||
|
let output = run(&["--fake-script", path_text(&invalid)]);
|
||||||
|
assert_exit(&output, EXIT_INPUT);
|
||||||
|
assert!(stderr(&output).contains("line 1"));
|
||||||
|
|
||||||
|
let cancelled = directory.write(
|
||||||
|
"cancelled.tsv",
|
||||||
|
&format!(
|
||||||
|
"!client\t{ALICE}\tAlice\tBot\n\
|
||||||
|
!cancel\n\
|
||||||
|
sleep 60\n\
|
||||||
|
!shutdown\n"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let output = run(&["--fake-script", path_text(&cancelled)]);
|
||||||
|
assert!(output.status.success(), "{}", stderr(&output));
|
||||||
|
let text = stdout(&output);
|
||||||
|
assert!(text.contains("Pause cancelled; agent resumed"), "{text}");
|
||||||
|
assert!(text.contains("CALL agent-pause"), "{text}");
|
||||||
|
assert!(text.contains("CALL agent-resume"), "{text}");
|
||||||
|
assert!(text.contains("connected=0"), "{text}");
|
||||||
|
assert!(text.contains("active-tasks=0 shutdown=true"), "{text}");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user