#![allow(
dead_code,
unused_imports,
clippy::expect_used,
clippy::panic,
clippy::unwrap_used
)]
#[path = "../src/control/mod.rs"]
mod control;
use control::{
AgentStatus, Axis, ClientIdentity, CommandResult, ErrorCode, Event, EventKind, EventQueue,
FocusTarget, PeerAuthorization, PublishOutcome, Reply, ReplyState, Request, TabAction,
WorkspaceAction, bind_control_socket, control_socket_path, decode_key_bytes,
decode_request_frame, error_reply, read_request, write_frame,
};
use std::collections::BTreeMap;
use std::fs;
use std::io::Cursor;
use std::os::unix::fs::{FileTypeExt, PermissionsExt};
use std::os::unix::net::UnixListener;
use std::path::PathBuf;
#[test]
fn every_command_has_a_strict_bounded_json_schema_and_echoes_its_id() {
let requests = vec![
Request::New {
id: 1,
cwd: None,
argv: vec!["sh".into()],
env: BTreeMap::new(),
},
Request::Split {
id: 2,
axis: Axis::Horizontal,
target: Some(1),
argv: vec!["sh".into()],
env: BTreeMap::new(),
},
Request::Focus {
id: 3,
target: FocusTarget::Left,
},
Request::Zoom { id: 4, pane: None },
Request::Kill { id: 5, pane: 1 },
Request::Resize {
id: 6,
pane: 1,
delta: 3,
},
Request::SendKeys {
id: 7,
pane: 1,
keys: "hello\\n".into(),
},
Request::Capture {
id: 8,
pane: 1,
attrs: true,
scrollback: 20,
max_bytes: 4096,
},
Request::List { id: 9 },
Request::Tab {
id: 10,
action: TabAction::Next,
},
Request::Workspace {
id: 11,
action: WorkspaceAction::List,
},
Request::SetStatus {
id: 12,
segment: "agent".into(),
text: "idle".into(),
},
Request::Popup {
id: 13,
rows: Some(20),
cols: Some(60),
argv: vec!["fzf".into()],
env: BTreeMap::new(),
},
Request::Subscribe {
id: 14,
events: vec![EventKind::AgentState],
},
];
for request in requests {
let decoded = decode_request_frame(&serde_json::to_vec(&request).expect("serialize"))
.expect("decode");
assert_eq!(decoded, request);
assert_eq!(decoded.id(), request.id());
}
}
#[test]
fn malformed_and_unknown_requests_return_structured_errors_without_losing_known_ids() {
let unknown = decode_request_frame(br#"{"command":"future","id":41}"#).expect_err("unknown");
assert_eq!(
(unknown.id, unknown.code),
(Some(41), ErrorCode::UnknownCommand)
);
let extra =
decode_request_frame(br#"{"command":"list","id":42,"extra":true}"#).expect_err("extra");
assert_eq!(extra.id, Some(42));
assert_eq!(error_reply(&extra).id(), 42);
let defaulted =
decode_request_frame(br#"{"command":"new","id":43,"cwd":null,"argv":[],"env":{}}"#)
.expect("empty argv selects the configured default");
assert_eq!(defaulted.id(), 43);
let bad = decode_request_frame(br#"{"command":"new","id":44,"cwd":null,"argv":[""],"env":{}}"#)
.expect_err("argv entry");
assert_eq!((bad.id, bad.code), (Some(44), ErrorCode::InvalidRequest));
}
#[test]
fn option_like_environment_names_are_rejected_before_spawning_env() {
for name in ["-u", "--x"] {
let frame = serde_json::to_vec(&Request::New {
id: 47,
cwd: None,
argv: vec!["true".into()],
env: BTreeMap::from([(name.to_owned(), "value".to_owned())]),
})
.expect("encode");
let error = decode_request_frame(&frame).expect_err("option-like env name");
assert_eq!(
(error.id, error.code),
(Some(47), ErrorCode::InvalidRequest)
);
}
let portable = Request::New {
id: 48,
cwd: None,
argv: vec!["true".into()],
env: BTreeMap::from([("_FUX_2".to_owned(), "value".to_owned())]),
};
portable.validate().expect("portable environment name");
}
#[test]
fn maximum_capture_payload_always_fits_the_json_frame() {
let request = Request::Capture {
id: 45,
pane: 1,
attrs: true,
scrollback: 0,
max_bytes: control::MAX_CAPTURE_BYTES,
};
request.validate().expect("maximum capture");
let too_large = Request::Capture {
id: 46,
pane: 1,
attrs: true,
scrollback: 0,
max_bytes: control::MAX_CAPTURE_BYTES + 1,
};
assert!(too_large.validate().is_err());
let reply = Reply::Completed {
id: 45,
result: CommandResult::Capture {
text: "\u{1b}".repeat(control::MAX_CAPTURE_BYTES),
},
};
assert!(serde_json::to_vec(&reply).expect("encode").len() <= control::MAX_FRAME_BYTES);
}
#[test]
fn popup_dimensions_are_rejected_before_any_host_side_spawn() {
let error = decode_request_frame(
br#"{"command":"popup","id":91,"rows":513,"cols":513,"argv":[],"env":{}}"#,
)
.expect_err("oversized popup");
assert_eq!(
(error.id, error.code),
(Some(91), ErrorCode::InvalidRequest)
);
}
#[test]
fn oversized_line_is_drained_so_the_following_request_remains_decodable() {
let mut bytes = vec![b'x'; control::MAX_FRAME_BYTES + 1];
bytes.extend_from_slice(b"\n{\"command\":\"list\",\"id\":9}\n");
let mut reader = Cursor::new(bytes);
assert_eq!(
read_request(&mut reader).expect_err("large").code,
ErrorCode::FrameTooLarge
);
assert_eq!(
read_request(&mut reader).expect("read").expect("frame"),
Request::List { id: 9 }
);
}
#[test]
fn replies_and_every_event_are_newline_delimited_and_keep_subscription_ids() {
let reply = Reply::Completed {
id: 5,
result: CommandResult::Unit,
};
assert_eq!(reply.state(), ReplyState::Completed);
let mut output = Vec::new();
write_frame(&mut output, &reply).expect("write");
assert_eq!(output.last(), Some(&b'\n'));
let events = [
Event::PaneOpened {
id: 77,
pane: 1,
command: vec!["sh".into()],
},
Event::PaneClosed {
id: 77,
pane: 1,
exit_status: Some(0),
},
Event::PaneFocused { id: 77, pane: 1 },
Event::PaneTitle {
id: 77,
pane: 1,
title: "shell".into(),
},
Event::AgentState {
id: 77,
pane: 1,
agent: Some("claude".into()),
old_state: AgentStatus::Working,
new_state: AgentStatus::Idle,
timestamp_ms: 1,
},
Event::PaneOutput { id: 77, pane: 1 },
Event::WorkspaceResized {
id: 77,
rows: 40,
cols: 120,
},
Event::ClientAttached {
id: 77,
client: ClientIdentity::Local,
},
Event::ClientDetached {
id: 77,
client: ClientIdentity::Endpoint("peer".into()),
},
];
assert_eq!(
serde_json::to_value(&events[0])
.expect("event JSON")
.get("event")
.and_then(serde_json::Value::as_str),
Some("pane.opened")
);
assert_eq!(
serde_json::to_string(&EventKind::AgentState).expect("event kind"),
"\"agent.state\""
);
for event in events {
assert_eq!(event.id(), 77);
assert_eq!(
serde_json::from_slice::<Event>(&serde_json::to_vec(&event).expect("encode"))
.expect("decode"),
event
);
}
}
#[test]
fn key_escape_decoder_rejects_incomplete_or_unknown_escapes() {
assert_eq!(
decode_key_bytes("a\\x1b\\nλ").expect("valid"),
b"a\x1b\n\xce\xbb"
);
assert!(decode_key_bytes("\\x1").is_err());
assert!(decode_key_bytes("\\q").is_err());
assert!(decode_key_bytes("tail\\").is_err());
}
#[test]
fn output_is_shed_before_state_events_and_persistently_slow_clients_disconnect() {
let (queue, receiver) = EventQueue::bounded(2).expect("queue");
assert_eq!(
queue.publish(Event::PaneOutput { id: 1, pane: 1 }),
PublishOutcome::Queued
);
assert_eq!(
queue.publish(Event::PaneFocused { id: 1, pane: 1 }),
PublishOutcome::Queued
);
assert_eq!(
queue.publish(Event::PaneTitle {
id: 1,
pane: 1,
title: "new".into()
}),
PublishOutcome::Queued
);
assert!(matches!(
receiver.try_recv(),
Some(Event::PaneFocused { .. })
));
assert!(matches!(receiver.try_recv(), Some(Event::PaneTitle { .. })));
queue.publish(Event::PaneFocused { id: 1, pane: 1 });
queue.publish(Event::PaneTitle {
id: 1,
pane: 1,
title: "x".into(),
});
assert_eq!(
queue.publish(Event::PaneClosed {
id: 1,
pane: 1,
exit_status: None
}),
PublishOutcome::DisconnectedSlowClient
);
assert!(receiver.is_disconnected());
}
#[test]
fn unix_socket_enforces_private_paths_and_conservative_stale_replacement() {
let root = test_directory();
fs::create_dir(&root).expect("root");
let bound = bind_control_socket(&root, "work").expect("bind");
assert_eq!(
bound.peer_authorization(),
PeerAuthorization::FilesystemPermissions
);
assert_eq!(
fs::metadata(root.join("fux"))
.expect("dir")
.permissions()
.mode()
& 0o777,
0o700
);
let metadata = fs::metadata(bound.path()).expect("socket");
assert!(metadata.file_type().is_socket());
assert_eq!(metadata.permissions().mode() & 0o777, 0o600);
assert_eq!(
bind_control_socket(&root, "work").expect_err("live").kind(),
std::io::ErrorKind::AddrInUse
);
drop(bound);
let protected = bind_control_socket(&root, "protected").expect("bind protected");
let protected_path = protected.path().to_owned();
let moved_path = root.join("fux/protected.old");
fs::rename(&protected_path, &moved_path).expect("move original socket");
let replacement = UnixListener::bind(&protected_path).expect("bind replacement");
drop(protected);
assert!(
fs::symlink_metadata(&protected_path)
.expect("replacement preserved")
.file_type()
.is_socket()
);
drop(replacement);
let stale_path = control_socket_path(&root, "stale").expect("path");
drop(UnixListener::bind(&stale_path).expect("stale"));
drop(bind_control_socket(&root, "stale").expect("replace"));
let occupied = control_socket_path(&root, "file").expect("path");
fs::write(&occupied, "keep").expect("file");
assert_eq!(
bind_control_socket(&root, "file").expect_err("file").kind(),
std::io::ErrorKind::AlreadyExists
);
assert_eq!(fs::read_to_string(&occupied).expect("preserved"), "keep");
assert!(control_socket_path(&root, "../escape").is_err());
fs::remove_dir_all(root).expect("cleanup");
}
fn test_directory() -> PathBuf {
let path = std::env::temp_dir().join(format!("fux-control-{}", std::process::id()));
let _ = fs::remove_dir_all(&path);
path
}