Files
tty7/crates/tty7-server/tests/machine_tree.rs
l0ng-aiandl0ng-ai 8c1946d763 chore: strip every comment from the Rust sources (#268)
Removed all Rust comments -- line, block, and doc -- from the 139 tracked
.rs files with `uncomment` 3.5.1. It parses each file with tree-sitter
instead of matching text, so comment-like content inside string literals
is left alone: the JavaScript plugin source embedded in agent_hooks.rs
raw strings keeps its own `//` lines.

Left alone: Cargo.toml comments and the shell scripts under scripts/.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-30 21:36:15 +08:00

463 lines
13 KiB
Rust

#![cfg(unix)]
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tty7_core::core::machine::{Axis, LayoutDelta, MACHINE_FILE, PaneNode, PaneSeed};
use tty7_core::daemon::control::{
ControlClient, ControlEvent, ControlHello, ControlRequest, LinkShutdown, ReplyOk, WorkspaceId,
feature,
};
struct ServerProcess {
child: Mutex<Option<Child>>,
}
impl LinkShutdown for ServerProcess {
fn shutdown_link(&self) -> io::Result<()> {
let Some(mut child) = self.child.lock().unwrap_or_else(|e| e.into_inner()).take() else {
return Ok(());
};
let _ = child.kill();
let _ = child.wait();
Ok(())
}
}
struct Client {
control: ControlClient,
events: Arc<Mutex<Vec<ControlEvent>>>,
peer_features: Vec<String>,
}
impl Client {
fn expect_delta(&self, workspace: WorkspaceId, want: impl Fn(&LayoutDelta) -> bool) {
let key = workspace.to_string();
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let seen = self
.events
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
if seen.iter().any(|e| {
matches!(e, ControlEvent::Layout { workspace: w, delta } if *w == key && want(delta))
}) {
return;
}
assert!(
Instant::now() < deadline,
"no matching Layout delta for {key}; saw {seen:?}"
);
std::thread::sleep(Duration::from_millis(20));
}
}
fn delta_count(&self) -> usize {
self.events
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.filter(|e| matches!(e, ControlEvent::Layout { .. }))
.count()
}
}
fn connect(data_dir: &Path, token: &str) -> Client {
let mut child = Command::new(env!("CARGO_BIN_EXE_tty7-server"))
.args(["--stdio", "--serve"])
.env("TTY7_DATA_DIR", data_dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("could not start tty7-server --stdio");
let stdout = child.stdout.take().expect("piped");
let stdin = child.stdin.take().expect("piped");
let closer: Arc<dyn LinkShutdown> = Arc::new(ServerProcess {
child: Mutex::new(Some(child)),
});
let events: Arc<Mutex<Vec<ControlEvent>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&events);
let control = ControlClient::connect_with(
stdout,
stdin,
Some(closer),
&ControlHello::host_rpc(token, "test-client"),
Box::new(move |event| sink.lock().unwrap_or_else(|e| e.into_inner()).push(event)),
)
.expect("handshake with tty7-server --stdio");
let peer_features = control.hello().features.clone();
Client {
control,
events,
peer_features,
}
}
fn data_dir() -> tempfile::TempDir {
tempfile::TempDir::new().unwrap()
}
fn machine_file(dir: &tempfile::TempDir) -> PathBuf {
dir.path().join(MACHINE_FILE)
}
fn seed(pane: u64, cwd: &str) -> PaneSeed {
PaneSeed {
pane,
cwd: Some(cwd.to_string()),
ssh_spec: None,
agent: None,
}
}
#[test]
fn the_server_advertises_the_machine_tree() {
let dir = data_dir();
let client = connect(dir.path(), "cap");
assert!(
client
.peer_features
.iter()
.any(|f| f == feature::MACHINE_TREE),
"features were {:?}",
client.peer_features
);
}
#[test]
fn the_tree_is_built_by_operations_and_lives_in_the_servers_file() {
let dir = data_dir();
let client = connect(dir.path(), "ops");
let ws = match client
.control
.call(ControlRequest::WorkspaceCreate {
name: Some("api".into()),
workspace: None,
})
.expect("create workspace")
{
ReplyOk::WorkspaceTree(ws) => *ws,
other => panic!("expected WorkspaceTree, got {other:?}"),
};
let tab = match client
.control
.call(ControlRequest::TabCreate {
workspace: ws.id,
at: None,
pane: seed(1, "/home/me/proj"),
tab: None,
})
.expect("create tab")
{
ReplyOk::TabTree(tab) => *tab,
other => panic!("expected TabTree, got {other:?}"),
};
client
.control
.call(ControlRequest::PaneSplit {
workspace: ws.id,
pane: 1,
axis: Axis::Vertical,
ratio: 0.3,
new: seed(2, "/home/me/proj/sub"),
first: false,
})
.expect("split");
let machine = match client.control.call(ControlRequest::MachineGet).unwrap() {
ReplyOk::MachineTree(m) => *m,
other => panic!("expected MachineTree, got {other:?}"),
};
assert_eq!(machine.workspaces.len(), 1);
assert_eq!(machine.workspaces[0].tabs[0].id, tab.id);
assert_eq!(machine.workspaces[0].tabs[0].root.pane_ids(), vec![1, 2]);
assert_eq!(machine.panes.len(), 2);
assert!(
machine.panes.iter().all(|p| p.live),
"panes this server was told about in its own lifetime are live"
);
let text = std::fs::read_to_string(machine_file(&dir)).expect("the server wrote its tree");
assert!(text.contains(&ws.id.to_string()), "{text}");
let missing = client
.control
.call(ControlRequest::WorkspaceTree {
workspace: WorkspaceId::new(),
})
.unwrap_err();
assert_eq!(missing.kind(), io::ErrorKind::NotFound);
}
#[test]
fn a_new_server_process_reports_the_old_panes_dead_and_accepts_their_successors() {
let dir = data_dir();
let ws = {
let first = connect(dir.path(), "first");
let ws = match first
.control
.call(ControlRequest::WorkspaceCreate {
name: None,
workspace: None,
})
.unwrap()
{
ReplyOk::WorkspaceTree(ws) => *ws,
other => panic!("{other:?}"),
};
first
.control
.call(ControlRequest::TabCreate {
workspace: ws.id,
at: None,
pane: seed(7, "/home/me/proj"),
tab: None,
})
.unwrap();
first.control.close();
ws
};
let second = connect(dir.path(), "second");
let machine = match second.control.call(ControlRequest::MachineGet).unwrap() {
ReplyOk::MachineTree(m) => *m,
other => panic!("{other:?}"),
};
let record = machine
.panes
.iter()
.find(|p| p.id == 7)
.expect("the pane record survives the restart");
assert!(!record.live, "a restarted server has no live panes");
assert_eq!(
record.cwd.as_deref(),
Some("/home/me/proj"),
"the facts a successor spawns from survive"
);
assert_eq!(
machine.workspaces[0].tabs[0].root,
PaneNode::Leaf { pane: 7 },
"the leaf still names the dead pane — the revival slot"
);
second
.control
.call(ControlRequest::PaneReplace {
workspace: ws.id,
old: 7,
new: seed(1, "/home/me/proj"),
})
.expect("replace");
let machine = match second.control.call(ControlRequest::MachineGet).unwrap() {
ReplyOk::MachineTree(m) => *m,
other => panic!("{other:?}"),
};
assert_eq!(
machine.workspaces[0].tabs[0].root,
PaneNode::Leaf { pane: 1 }
);
assert!(machine.panes.iter().all(|p| p.id != 7));
}
#[test]
fn an_operation_from_one_client_reaches_the_other_as_a_delta() {
use tty7_core::host::local::LocalHost;
use tty7_core::host::server;
let dir = data_dir();
let machine = tty7_core::core::machine::MachineStore::open(machine_file(&dir));
let sock = dir.path().join("control.sock");
let listener = server::bind_control_socket(&sock).unwrap();
{
let machine = Arc::clone(&machine);
std::thread::spawn(move || {
server::serve_listener_with(
listener,
LocalHost::new(),
server::Services::with_machine(machine),
)
});
}
let writer = bridged(&sock, "writer");
let watcher = bridged(&sock, "watcher");
assert!(
writer
.peer_features
.iter()
.any(|f| f == feature::MACHINE_TREE)
);
watcher.control.call(ControlRequest::Ping).unwrap();
let ws = match writer
.control
.call(ControlRequest::WorkspaceCreate {
name: Some("shared".into()),
workspace: None,
})
.unwrap()
{
ReplyOk::WorkspaceTree(ws) => *ws,
other => panic!("{other:?}"),
};
let tab = match writer
.control
.call(ControlRequest::TabCreate {
workspace: ws.id,
at: None,
pane: seed(3, "/srv"),
tab: None,
})
.unwrap()
{
ReplyOk::TabTree(tab) => *tab,
other => panic!("{other:?}"),
};
watcher.expect_delta(
ws.id,
|d| matches!(d, LayoutDelta::WorkspaceCreated { workspace } if workspace.id == ws.id),
);
watcher.expect_delta(
ws.id,
|d| matches!(d, LayoutDelta::TabCreated { tab: t, .. } if t.id == tab.id),
);
watcher.expect_delta(
ws.id,
|d| matches!(d, LayoutDelta::ActiveTabChanged { tab: t } if *t == tab.id),
);
assert_eq!(
writer.delta_count(),
0,
"a client must not be pushed its own operation"
);
watcher
.control
.call(ControlRequest::TabRename {
workspace: ws.id,
tab: tab.id,
name: Some("build".into()),
})
.unwrap();
writer.expect_delta(
ws.id,
|d| matches!(d, LayoutDelta::TabRenamed { name: Some(n), .. } if n == "build"),
);
assert_eq!(watcher.delta_count(), 3, "still only the writer's own ops");
}
#[test]
fn attachment_rides_the_tree_when_no_record_store_is_served() {
use tty7_core::host::local::LocalHost;
use tty7_core::host::server;
let dir = data_dir();
let machine = tty7_core::core::machine::MachineStore::open(machine_file(&dir));
let sock = dir.path().join("control.sock");
let listener = server::bind_control_socket(&sock).unwrap();
{
let machine = Arc::clone(&machine);
std::thread::spawn(move || {
server::serve_listener_with(
listener,
LocalHost::new(),
server::Services::with_machine(machine),
)
});
}
let ws = machine
.workspace_create(None, Some("shared".into()), None)
.unwrap();
let laptop = bridged(&sock, "laptop");
let desktop = bridged(&sock, "desktop");
let attach = |client: &Client| {
client.control.call(ControlRequest::WorkspaceAttach {
id: ws.id.to_string(),
})
};
match attach(&laptop).expect("first attach") {
ReplyOk::Attached { took_over_from } => assert_eq!(took_over_from, None),
other => panic!("{other:?}"),
}
assert_eq!(
machine.attachment(ws.id).map(|a| a.hostname),
Some("laptop".into()),
"the tree's own record says who holds the workspace"
);
match attach(&desktop).expect("takeover") {
ReplyOk::Attached { took_over_from } => {
assert_eq!(took_over_from.as_deref(), Some("laptop"));
}
other => panic!("{other:?}"),
}
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let seen = laptop.events.lock().unwrap().clone();
if seen.iter().any(|e| {
matches!(e, ControlEvent::Preempted { workspace, by }
if *workspace == ws.id.to_string() && by == "desktop")
}) {
break;
}
assert!(Instant::now() < deadline, "no Preempted push; saw {seen:?}");
std::thread::sleep(Duration::from_millis(20));
}
laptop
.control
.call(ControlRequest::WorkspaceDetach {
id: ws.id.to_string(),
})
.expect("a stale detach is success, not eviction");
assert_eq!(
machine.attachment(ws.id).map(|a| a.hostname),
Some("desktop".into())
);
}
fn bridged(sock: &Path, token: &str) -> Client {
let hello = ControlHello::host_rpc(token, token);
let mut child = Command::new(env!("CARGO_BIN_EXE_tty7-server"))
.args(["--stdio", "--bridge", "--control-sock"])
.arg(sock)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("could not start the bridging client");
let stdout = child.stdout.take().expect("piped");
let stdin = child.stdin.take().expect("piped");
let closer: Arc<dyn LinkShutdown> = Arc::new(ServerProcess {
child: Mutex::new(Some(child)),
});
let events: Arc<Mutex<Vec<ControlEvent>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&events);
let control = ControlClient::connect_with(
stdout,
stdin,
Some(closer),
&hello,
Box::new(move |e| sink.lock().unwrap_or_else(|e| e.into_inner()).push(e)),
)
.expect("bridge handshake");
let peer_features = control.hello().features.clone();
Client {
control,
events,
peer_features,
}
}