Implement SimpleBot native client (#86)
Some checks failed
Native code generation / deterministic (push) Successful in 18m9s
Imaging and meshing gate / native (push) Successful in 5m45s
JPEG 2000 feature / linux (push) Successful in 2m45s
Native Rust workspace compile / compile (push) Has been cancelled
Skia feature / linux (push) Successful in 31m33s

This commit is contained in:
2026-08-11 07:29:44 +00:00
parent f93a27f469
commit f0fd975d0f
12 changed files with 1240 additions and 10 deletions

View File

@@ -0,0 +1,204 @@
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
const SOURCE_ID: &str = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
const BOT_ID: &str = "11111111-2222-3333-4444-555555555555";
const DANCE1: &str = "b68a3d7c-de9e-fc87-eec8-543d787e5b0d";
const EXIT_USAGE: i32 = 2;
const EXIT_INPUT: i32 = 3;
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
struct TestDir(PathBuf);
impl TestDir {
fn new(test_name: &str) -> Self {
let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"metacrate-simple-bot-{test_name}-{}-{id}",
std::process::id()
));
fs::create_dir(&path).expect("create isolated 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 run(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_simple-bot"))
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("run simple-bot")
}
fn utf8(bytes: &[u8]) -> &str {
std::str::from_utf8(bytes).expect("command output is UTF-8")
}
fn path_text(path: &Path) -> &str {
path.to_str().expect("test path is UTF-8")
}
fn assert_exit(output: &Output, expected: i32) {
assert_eq!(
output.status.code(),
Some(expected),
"stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn help_documents_upstream_arguments_and_commands() {
let output = run(&["--help"]);
assert!(output.status.success());
let help = utf8(&output.stdout);
for marker in [
"[FIRSTNAME]",
"[LASTNAME]",
"[PASSWORD]",
"--fake-script",
"help, ?",
"where, location",
"sit",
"stand",
"dance",
"fly",
"walk",
"jump",
"hello, hi, hey",
] {
assert!(help.contains(marker), "help omitted {marker}:\n{help}");
}
let output = run(&[]);
assert_exit(&output, EXIT_USAGE);
assert!(utf8(&output.stderr).contains("Usage: simple-bot"));
}
#[test]
fn scripted_fake_grid_covers_every_command_call_and_reply() {
let directory = TestDir::new("commands");
let script = directory.path("conversation.tsv");
let contents = format!(
"# deterministic fake-grid conversation\n\
im\t{SOURCE_ID}\tAlice Resident\thelp\n\
im\t{SOURCE_ID}\tAlice Resident\tlocation\n\
im\t{SOURCE_ID}\tAlice Resident\tsit\n\
im\t{SOURCE_ID}\tAlice Resident\tstand\n\
im\t{SOURCE_ID}\tAlice Resident\tdance\n\
im\t{SOURCE_ID}\tAlice Resident\tfly\n\
im\t{SOURCE_ID}\tAlice Resident\twalk\n\
im\t{SOURCE_ID}\tAlice Resident\tjump\n\
im\t{SOURCE_ID}\tAlice Resident\thi\n\
im\t{SOURCE_ID}\tAlice Resident\tunknown\n\
chat\t{SOURCE_ID}\tAlice Resident\thello bot\n\
im\t{BOT_ID}\tSimple Bot\thelp\n\
chat\t{BOT_ID}\tSimple Bot\thello self\n\
status\tconnected to https://example.invalid/cap/private\n"
);
fs::write(&script, contents).expect("write conversation script");
let output = run(&["--fake-script", path_text(&script)]);
assert!(output.status.success(), "{}", utf8(&output.stderr));
assert!(output.stderr.is_empty());
let output = utf8(&output.stdout);
for expected in [
"Fake grid ready: Scripted Test Region at <128, 64, 25>",
"Commands: help, where, sit, stand, dance, fly, walk, jump, hello, hi, hey",
"I'm in Scripted Test Region at <128, 64, 25>",
"CALL sit-on-ground",
"Sitting down...",
"CALL stand",
"Standing up!",
&format!("CALL animation-start {DANCE1} reliable=true"),
"Dancing!",
"CALL fly true",
"Taking off!",
"CALL fly false",
"Walking now.",
"CALL jump true",
"WAIT 500ms",
"CALL jump false",
"Wheee!",
"Hello, Alice Resident!",
"I don't understand that command.",
"WAIT 750ms",
"CALL chat channel=0 type=Normal Hello, Alice Resident!",
"connected to <redacted-url>",
"Fake grid logout complete; active_tasks=0 open_sockets=0",
] {
assert!(
output.contains(expected),
"output omitted {expected}:\n{output}"
);
}
assert!(!output.contains("hello self"));
assert!(!output.contains("[IM] Simple Bot"));
assert!(!output.contains("/cap/private"));
}
#[test]
fn fake_grid_redacts_sensitive_messages_and_never_echoes_cli_passwords() {
let directory = TestDir::new("redaction");
let script = directory.path("redaction.tsv");
fs::write(
&script,
format!("im\t{SOURCE_ID}\tAlice Resident\ttoken=private-value\n"),
)
.expect("write redaction script");
let output = run(&["--fake-script", path_text(&script)]);
assert!(output.status.success());
assert!(!utf8(&output.stdout).contains("private-value"));
assert!(utf8(&output.stdout).contains("<redacted>"));
let password = "super-secret-password";
let output = run(&[
"First",
"Last",
password,
"--fake-script",
path_text(&script),
]);
assert_exit(&output, EXIT_USAGE);
assert!(!utf8(&output.stdout).contains(password));
assert!(!utf8(&output.stderr).contains(password));
}
#[test]
fn malformed_or_oversized_scripts_fail_without_partial_output() {
let directory = TestDir::new("invalid");
let malformed = directory.path("malformed.tsv");
fs::write(&malformed, "im\tnot-a-uuid\tAlice Resident\thelp\n")
.expect("write malformed script");
let output = run(&["--fake-script", path_text(&malformed)]);
assert_exit(&output, EXIT_INPUT);
assert!(output.stdout.is_empty());
assert!(utf8(&output.stderr).contains("source UUID is invalid"));
let oversized = directory.path("oversized.tsv");
let mut file = fs::File::create(&oversized).expect("create oversized script");
file.write_all(&vec![b'x'; 1024 * 1024 + 1])
.expect("write oversized script");
drop(file);
let output = run(&["--fake-script", path_text(&oversized)]);
assert_exit(&output, EXIT_INPUT);
assert!(output.stdout.is_empty());
assert!(utf8(&output.stderr).contains("1 MiB limit"));
}