aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 23:33:27 +0300
committerGitHub <noreply@github.com>2026-09-10 23:33:27 +0300
commit043b804c44665b07bd503fdad896bbe0a29311a0 (patch)
tree79bae0b64ba1c0e22328b0401196587633399c60 /tests
parent425c86179791cd32e2d4ce0ae7f269940b4b0663 (diff)
feat: route cli commands to saved ssh machines (#3918)
Diffstat (limited to 'tests')
-rw-r--r--tests/machine_api.rs404
1 files changed, 404 insertions, 0 deletions
diff --git a/tests/machine_api.rs b/tests/machine_api.rs
new file mode 100644
index 00000000..895e440a
--- /dev/null
+++ b/tests/machine_api.rs
@@ -0,0 +1,404 @@
+#![cfg(all(unix, not(target_os = "macos")))]
+
+use std::fs;
+use std::io::{BufRead, BufReader, Write};
+use std::os::unix::fs::PermissionsExt;
+use std::os::unix::net::UnixListener;
+use std::path::PathBuf;
+use std::process::{Command, Output};
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::time::{Duration, Instant};
+
+use serde_json::{json, Value};
+
+const PROFILE_ID: &str = "0123456789abcdef0123456789abcdef";
+const SSH: &str = r#"#!/bin/sh
+for arg do
+ last=$arg
+ printf '%s\n' "$arg" >> "$TEST_ROOT/ssh-args"
+done
+case "$last" in
+ *'command -v herdr') printf 'login banner\nherdr-remote-output-ready:1\n%s\n' "$TEST_REMOTE_HERDR" ;;
+ *'remote-api-bridge --check')
+ if [ "$TEST_MODE" = old ]; then exit 2; fi
+ exec /bin/sh -c "$last" ;;
+ *'remote-api-bridge')
+ if [ "$TEST_MODE" = offline ]; then echo 'test remote connection failed' >&2; exit 255; fi
+ exec /bin/sh -c "$last" ;;
+ '/bin/sh -s')
+ script=$(cat)
+ printf 'login banner\nherdr-remote-output-ready:1\n'
+ case "$script" in
+ *'uname -s'*) uname -s; uname -m ;;
+ *'version='*) printf '%s\n' "$TEST_REMOTE_HERDR" ;;
+ *) echo "unexpected discovery: $script" >&2; exit 2 ;;
+ esac ;;
+ *) echo "unexpected command: $last" >&2; exit 2 ;;
+esac
+"#;
+
+struct Harness {
+ root: PathBuf,
+ remote: UnixListener,
+ local: UnixListener,
+ protocol: u64,
+}
+
+impl Harness {
+ fn new() -> Self {
+ static NEXT: AtomicU64 = AtomicU64::new(0);
+ let root = PathBuf::from(format!(
+ "/var/tmp/hma-{}-{}",
+ std::process::id(),
+ NEXT.fetch_add(1, Ordering::Relaxed)
+ ));
+ let app = if cfg!(debug_assertions) {
+ "herdr-dev"
+ } else {
+ "herdr"
+ };
+ let state = root.join("state").join(app).join("client");
+ let session = root.join("config").join(app).join("sessions/fleet");
+ fs::create_dir_all(&state).unwrap();
+ fs::create_dir_all(&session).unwrap();
+ fs::create_dir_all(root.join("bin")).unwrap();
+ fs::write(root.join("bin/ssh"), SSH).unwrap();
+ fs::set_permissions(root.join("bin/ssh"), fs::Permissions::from_mode(0o700)).unwrap();
+ std::os::unix::fs::symlink(env!("CARGO_BIN_EXE_herdr"), root.join("remote herdr")).unwrap();
+ fs::write(state.join("endpoints.json"), serde_json::to_vec(&json!({
+ "version": 1,
+ "ssh": [{"id": PROFILE_ID, "label": "mac", "target": "fake-mac", "session": "fleet", "enabled": true}]
+ })).unwrap()).unwrap();
+ let remote = UnixListener::bind(session.join("herdr.sock")).unwrap();
+ remote.set_nonblocking(true).unwrap();
+ let local = UnixListener::bind(root.join("local.sock")).unwrap();
+ local.set_nonblocking(true).unwrap();
+ let status = Command::new(env!("CARGO_BIN_EXE_herdr"))
+ .args(["status", "client", "--json"])
+ .output()
+ .unwrap();
+ let status: Value = serde_json::from_slice(&status.stdout).unwrap();
+ Self {
+ root,
+ remote,
+ local,
+ protocol: status["protocol"].as_u64().unwrap(),
+ }
+ }
+
+ fn command(&self, args: &[&str]) -> Command {
+ let mut command = Command::new(env!("CARGO_BIN_EXE_herdr"));
+ command
+ .args(args)
+ .env(
+ "PATH",
+ format!("{}:/usr/bin:/bin", self.root.join("bin").display()),
+ )
+ .env("HOME", &self.root)
+ .env("XDG_CONFIG_HOME", self.root.join("config"))
+ .env("XDG_STATE_HOME", self.root.join("state"))
+ .env("XDG_RUNTIME_DIR", &self.root)
+ .env("TEST_ROOT", &self.root)
+ .env("TEST_REMOTE_HERDR", self.root.join("remote herdr"))
+ .env("HERDR_SOCKET_PATH", self.root.join("local.sock"))
+ .env(
+ "HERDR_CLIENT_SOCKET_PATH",
+ self.root.join("never-client.sock"),
+ )
+ .env("HERDR_SESSION", "wrong-inherited-session")
+ .env("HERDR_PANE_ID", "wrong-local-pane")
+ .env_remove("HERDR_CONFIG_PATH")
+ .env_remove("HERDR_REMOTE_BINARY");
+ command
+ }
+
+ fn serve(&self, result: Value, protocol: u64) -> std::thread::JoinHandle<Value> {
+ let listener = self.remote.try_clone().unwrap();
+ std::thread::spawn(move || {
+ let deadline = Instant::now() + Duration::from_secs(15);
+ loop {
+ let (mut stream, _) = match listener.accept() {
+ Ok(connection) => connection,
+ Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
+ assert!(
+ Instant::now() < deadline,
+ "remote API request did not arrive"
+ );
+ std::thread::sleep(Duration::from_millis(5));
+ continue;
+ }
+ Err(error) => panic!("{error}"),
+ };
+ stream
+ .set_read_timeout(Some(Duration::from_secs(5)))
+ .unwrap();
+ let mut line = String::new();
+ BufReader::new(stream.try_clone().unwrap())
+ .read_line(&mut line)
+ .unwrap();
+ let request: Value = serde_json::from_str(&line).unwrap();
+ let ping = request["method"] == "ping";
+ let response = if ping {
+ json!({"id": request["id"], "result": {"type": "pong", "version": "test", "protocol": protocol}})
+ } else {
+ std::thread::sleep(Duration::from_millis(50));
+ let mut response = result.clone();
+ response["id"] = request["id"].clone();
+ response
+ };
+ writeln!(stream, "{response}").unwrap();
+ if !ping || protocol == 0 {
+ return request;
+ }
+ }
+ })
+ }
+
+ fn assert_local_untouched(&self) {
+ assert_eq!(
+ self.local.accept().unwrap_err().kind(),
+ std::io::ErrorKind::WouldBlock
+ );
+ assert!(!self.root.join("never-client.sock").exists());
+ assert!(!fs::read_dir(&self.root).unwrap().any(|entry| entry
+ .unwrap()
+ .file_name()
+ .to_string_lossy()
+ .starts_with("herdr-api-")));
+ }
+}
+
+impl Drop for Harness {
+ fn drop(&mut self) {
+ let _ = fs::remove_dir_all(&self.root);
+ }
+}
+
+fn success(output: Output) -> Value {
+ assert!(
+ output.status.success(),
+ "{}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ serde_json::from_slice(&output.stdout).unwrap()
+}
+
+#[test]
+fn machine_api_routes_structured_payload_and_remote_errors_without_local_fallback() {
+ let harness = Harness::new();
+ let server = harness.serve(
+ json!({"error":{"code":"test_remote_error","message":"remote rejected prompt"}}),
+ harness.protocol,
+ );
+ let prompt = "quotes ' \" ; $(touch should-not-exist)\n--machine other";
+ let output = harness
+ .command(&["--machine", "mac", "agent", "prompt", "w4:p1", prompt])
+ .output()
+ .unwrap();
+ assert_eq!(output.status.code(), Some(1));
+ let error: Value = serde_json::from_slice(&output.stderr)
+ .unwrap_or_else(|error| panic!("{error}: {}", String::from_utf8_lossy(&output.stderr)));
+ assert_eq!(error["error"]["code"], "test_remote_error");
+ let request = server.join().unwrap();
+ assert_eq!(request["method"], "agent.prompt");
+ assert_eq!(request["params"]["text"], prompt);
+ let ssh_args = fs::read_to_string(harness.root.join("ssh-args")).unwrap();
+ assert!(ssh_args.contains("StrictHostKeyChecking=yes"));
+ assert!(ssh_args.contains("BatchMode=yes"));
+ assert!(ssh_args.contains("--session fleet remote-api-bridge"));
+ assert!(!ssh_args.contains("should-not-exist"));
+ harness.assert_local_untouched();
+}
+
+#[test]
+fn machine_api_profile_id_routes_large_list_responses() {
+ let harness = Harness::new();
+ let data = "remote data ".repeat(20_000);
+ let server = harness.serve(
+ json!({"result":{"type":"agent_list","agents":[],"test_data":data}}),
+ harness.protocol,
+ );
+ let response = success(
+ harness
+ .command(&["--machine", PROFILE_ID, "agent", "list"])
+ .output()
+ .unwrap(),
+ );
+ assert_eq!(response["result"]["test_data"], data);
+ assert_eq!(server.join().unwrap()["method"], "agent.list");
+ harness.assert_local_untouched();
+}
+
+#[test]
+fn machine_api_never_inherits_the_callers_pane() {
+ let harness = Harness::new();
+ let server = harness.serve(json!({"result":{"type":"ok"}}), harness.protocol);
+ success(
+ harness
+ .command(&["--machine=mac", "pane", "current"])
+ .output()
+ .unwrap(),
+ );
+ let request = server.join().unwrap();
+ assert_eq!(request["method"], "pane.current");
+ assert!(request["params"]["caller_pane_id"].is_null());
+ harness.assert_local_untouched();
+}
+
+#[test]
+fn machine_api_remote_paths_and_wait_parameters_reach_the_server() {
+ for (args, method, expected) in [
+ (
+ vec![
+ "worktree",
+ "create",
+ "--cwd",
+ "~/Projects/herdr",
+ "--branch",
+ "review",
+ "--path",
+ "/srv/review",
+ ],
+ "worktree.create",
+ json!({"cwd":"~/Projects/herdr", "branch":"review", "path":"/srv/review"}),
+ ),
+ (
+ vec![
+ "agent",
+ "wait",
+ "w4:p1",
+ "--until",
+ "idle",
+ "--timeout",
+ "2000",
+ ],
+ "agent.wait",
+ json!({"target":"w4:p1", "until":["idle"], "timeout_ms":2000}),
+ ),
+ ] {
+ let harness = Harness::new();
+ let server = harness.serve(json!({"result":{"type":"ok"}}), harness.protocol);
+ let mut command = harness.command(&["--machine", "mac"]);
+ success(command.args(args).output().unwrap());
+ let request = server.join().unwrap();
+ assert_eq!(request["method"], method);
+ for (key, value) in expected.as_object().unwrap() {
+ assert_eq!(&request["params"][key], value);
+ }
+ harness.assert_local_untouched();
+ }
+}
+
+#[test]
+fn machine_api_status_reports_remote_identity_not_local_installation_state() {
+ let harness = Harness::new();
+ let server = harness.serve(json!({}), 0);
+ let status = success(
+ harness
+ .command(&["--machine", "mac", "status", "server", "--json"])
+ .output()
+ .unwrap(),
+ );
+ assert_eq!(status["session"], "fleet");
+ assert_eq!(status["socket"], format!("machine:{PROFILE_ID}/fleet"));
+ assert!(status["server_binary_stale"].is_null());
+ assert_eq!(server.join().unwrap()["method"], "ping");
+ harness.assert_local_untouched();
+}
+
+#[test]
+fn machine_api_usage_errors_do_not_connect() {
+ let harness = Harness::new();
+ for args in [
+ vec!["--machine", "missing", "agent", "list"],
+ vec!["--machine", "mac", "config", "reset-keys"],
+ vec![
+ "--machine",
+ "mac",
+ "agent",
+ "explain",
+ "--file",
+ "/any/file",
+ "--agent",
+ "pi",
+ ],
+ vec![
+ "--machine",
+ "mac",
+ "pane",
+ "split",
+ "--current",
+ "--direction",
+ "right",
+ ],
+ vec!["--machine", "mac", "--session", "local", "agent", "list"],
+ ] {
+ assert_eq!(
+ harness.command(&args).output().unwrap().status.code(),
+ Some(2)
+ );
+ }
+ assert!(!harness.root.join("ssh-args").exists());
+ harness.assert_local_untouched();
+}
+
+#[test]
+fn machine_api_rejects_old_bridges_and_disconnected_machines() {
+ for (mode, message) in [
+ ("old", "update Herdr"),
+ ("offline", "test remote connection failed"),
+ ] {
+ let harness = Harness::new();
+ let output = harness
+ .command(&["--machine", "mac", "agent", "list"])
+ .env("TEST_MODE", mode)
+ .output()
+ .unwrap();
+ assert!(!output.status.success());
+ assert!(
+ String::from_utf8_lossy(&output.stderr).contains(message),
+ "{}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ harness.assert_local_untouched();
+ }
+}
+
+#[test]
+fn machine_api_server_stop_is_sent_only_to_the_selected_machine() {
+ let harness = Harness::new();
+ let server = harness.serve(json!({"result":{"type":"ok"}}), harness.protocol);
+ let output = harness
+ .command(&["--machine", "mac", "server", "stop"])
+ .env("HERDR_SOCKET_PATH", harness.root.join("missing-local.sock"))
+ .output()
+ .unwrap();
+ assert!(
+ output.status.success(),
+ "{}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(
+ harness.root.join("ssh-args").exists(),
+ "server stop bypassed machine routing"
+ );
+ assert_eq!(server.join().unwrap()["method"], "server.stop");
+ harness.assert_local_untouched();
+}
+
+#[test]
+fn machine_api_protocol_mismatch_never_sends_the_mutation() {
+ let harness = Harness::new();
+ let server = harness.serve(json!({}), 0);
+ let output = harness
+ .command(&["--machine", "mac", "pane", "close", "w4:p1"])
+ .output()
+ .unwrap();
+ assert_eq!(output.status.code(), Some(1));
+ assert_eq!(server.join().unwrap()["method"], "ping");
+ let error = String::from_utf8_lossy(&output.stderr);
+ assert!(error.contains("protocol_mismatch"), "{error}");
+ assert!(error.contains("machine 'mac'"), "{error}");
+ assert!(!error.contains("HERDR_SOCKET_PATH="), "{error}");
+ harness.assert_local_untouched();
+}