mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-21 16:01:04 +00:00
405 lines
14 KiB
Rust
405 lines
14 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
|
|
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();
|
|
}
|