Some checks failed
Native Rust workspace compile / compile (push) Failing after 12m49s
274 lines
9.5 KiB
Rust
274 lines
9.5 KiB
Rust
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}");
|
|
}
|