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