Files
herdr/tests/machine_api.rs

655 lines
21 KiB
Rust

#![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
printf 'ssh\n' >> "$TEST_ROOT/ssh-calls"
for arg do
last=$arg
printf '%s\n' "$arg" >> "$TEST_ROOT/ssh-args"
done
case "$last" in
'/bin/sh -c '*)
if [ "$TEST_MODE" = offline ]; then echo 'test remote connection failed' >&2; exit 255; fi
printf 'login banner\n'
PATH="$TEST_ROOT/remote bin:/usr/bin:/bin" 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 ;;
*) echo "unexpected discovery: $script" >&2; exit 2 ;;
esac ;;
*) echo "unexpected command: $last" >&2; exit 2 ;;
esac
"#;
struct Harness {
root: PathBuf,
state: 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::create_dir_all(root.join("remote bin")).unwrap();
let remote_wrapper = root.join("remote bin/herdr");
fs::write(
&remote_wrapper,
r#"#!/bin/sh
if [ "$TEST_MODE" = old ]; then printf 'herdr-api-bridge-v1\n'; exit 2; fi
case "$*" in
*'--check')
if IFS= read -r request; then
echo 'capability check consumed API stdin' >&2
exit 2
fi ;;
esac
exec "$TEST_REMOTE_HERDR" "$@"
"#,
)
.unwrap();
fs::set_permissions(&remote_wrapper, fs::Permissions::from_mode(0o700)).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,
state,
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";
if !ping && result.is_null() {
return request;
}
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 warm_metadata(&self) {
let server = self.serve(json!({}), 0);
success(
self.command(&["--machine", "mac", "status", "server", "--json"])
.output()
.unwrap(),
);
assert_eq!(server.join().unwrap()["method"], "ping");
}
fn ssh_calls(&self) -> usize {
fs::read_to_string(self.root.join("ssh-calls"))
.unwrap_or_default()
.lines()
.count()
}
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("remote-api-bridge"));
assert!(ssh_args.contains("fleet"));
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");
assert_eq!(
fs::read_to_string(harness.root.join("ssh-calls"))
.unwrap()
.lines()
.count(),
4,
"cold command: platform, discovery, protocol ping, request"
);
harness.assert_local_untouched();
}
#[test]
fn machine_api_bootstrap_falls_back_from_an_old_path_binary() {
let harness = Harness::new();
fs::create_dir_all(harness.root.join(".local/bin")).unwrap();
std::os::unix::fs::symlink(
env!("CARGO_BIN_EXE_herdr"),
harness.root.join(".local/bin/herdr"),
)
.unwrap();
let server = harness.serve(
json!({"result":{"type":"agent_list","agents":[]}}),
harness.protocol,
);
success(
harness
.command(&["--machine", "mac", "agent", "list"])
.env("TEST_MODE", "old")
.output()
.unwrap(),
);
assert_eq!(server.join().unwrap()["method"], "agent.list");
harness.assert_local_untouched();
}
#[test]
fn machine_api_reuses_discovery_across_commands_without_rewriting_profiles() {
let harness = Harness::new();
let catalog_before = fs::read(harness.state.join("endpoints.json")).unwrap();
for expected_calls in [None, Some(2)] {
let before = harness.ssh_calls();
let server = harness.serve(
json!({"result":{"type":"agent_list","agents":[]}}),
harness.protocol,
);
success(
harness
.command(&["--machine", "mac", "agent", "list"])
.output()
.unwrap(),
);
assert_eq!(server.join().unwrap()["method"], "agent.list");
if let Some(expected) = expected_calls {
assert_eq!(
harness.ssh_calls() - before,
expected,
"warm commands must skip discovery"
);
}
}
let before = harness.ssh_calls();
let server = harness.serve(json!({}), 0);
success(
harness
.command(&["--machine", "mac", "status", "server", "--json"])
.output()
.unwrap(),
);
assert_eq!(server.join().unwrap()["method"], "ping");
assert_eq!(
harness.ssh_calls() - before,
1,
"warm status needs only one SSH"
);
assert_eq!(
fs::read(harness.state.join("endpoints.json")).unwrap(),
catalog_before
);
harness.assert_local_untouched();
}
#[test]
fn machine_api_recovers_a_stale_path_before_sending_a_mutation() {
let harness = Harness::new();
harness.warm_metadata();
fs::remove_file(harness.root.join("remote bin/herdr")).unwrap();
fs::create_dir_all(harness.root.join(".local/bin")).unwrap();
std::os::unix::fs::symlink(
env!("CARGO_BIN_EXE_herdr"),
harness.root.join(".local/bin/herdr"),
)
.unwrap();
let before = harness.ssh_calls();
let server = harness.serve(json!({"result":{"type":"ok"}}), harness.protocol);
success(
harness
.command(&["--machine", "mac", "pane", "close", "w4:p1"])
.output()
.unwrap(),
);
assert_eq!(server.join().unwrap()["method"], "pane.close");
assert_eq!(
harness.ssh_calls() - before,
5,
"one failed ping followed by fresh discovery and one command"
);
let before = harness.ssh_calls();
harness.warm_metadata();
assert_eq!(
harness.ssh_calls() - before,
1,
"recovered path must be saved"
);
harness.assert_local_untouched();
}
#[test]
fn machine_api_never_replays_a_mutation_when_its_response_is_lost() {
let harness = Harness::new();
harness.warm_metadata();
let before = harness.ssh_calls();
let server = harness.serve(Value::Null, harness.protocol);
let output = harness
.command(&["--machine", "mac", "pane", "close", "w4:p1"])
.output()
.unwrap();
assert!(!output.status.success());
assert_eq!(server.join().unwrap()["method"], "pane.close");
assert_eq!(
harness.ssh_calls() - before,
2,
"mutation must not trigger rediscovery or replay"
);
harness.assert_local_untouched();
}
#[test]
fn machine_api_transient_connection_failure_keeps_working_metadata() {
let harness = Harness::new();
harness.warm_metadata();
let before = harness.ssh_calls();
let output = harness
.command(&["--machine", "mac", "agent", "list"])
.env("TEST_MODE", "offline")
.output()
.unwrap();
assert!(!output.status.success());
assert_eq!(
harness.ssh_calls() - before,
1,
"network failures must not retry"
);
let before = harness.ssh_calls();
harness.warm_metadata();
assert_eq!(
harness.ssh_calls() - before,
1,
"transient failure must not discard metadata"
);
}
#[test]
fn machine_remove_deletes_only_that_profiles_metadata() {
let harness = Harness::new();
harness.warm_metadata();
let directory = harness.state.join("ssh-metadata");
let other = directory.join("fedcba9876543210fedcba9876543210.json");
fs::write(&other, "other machine metadata").unwrap();
let before = harness.ssh_calls();
let output = harness
.command(&["machine", "remove", PROFILE_ID])
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(!directory.join(format!("{PROFILE_ID}.json")).exists());
assert_eq!(fs::read_to_string(other).unwrap(), "other machine metadata");
assert_eq!(harness.ssh_calls(), before);
harness.assert_local_untouched();
}
#[test]
fn machine_api_bad_or_unwritable_metadata_does_not_block_commands() {
let harness = Harness::new();
harness.warm_metadata();
let path = harness
.state
.join("ssh-metadata")
.join(format!("{PROFILE_ID}.json"));
fs::write(&path, "broken metadata").unwrap();
let before = harness.ssh_calls();
harness.warm_metadata();
assert_eq!(harness.ssh_calls() - before, 3);
fs::remove_file(&path).unwrap();
fs::create_dir(&path).unwrap();
for _ in 0..2 {
let before = harness.ssh_calls();
harness.warm_metadata();
assert_eq!(harness.ssh_calls() - before, 3);
}
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");
assert_eq!(
fs::read_to_string(harness.root.join("ssh-calls"))
.unwrap()
.lines()
.count(),
3,
"cold status: platform, discovery, status"
);
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();
harness.warm_metadata();
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();
}