mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-05 08:02:13 +00:00
Manual testing found `tty7 run`, `send`, `capture`, `procs` and `split` broken against any normally-installed server — the CLI's entire hot path. Only the control verbs worked. Two endpoints, two rules. The pane socket came from the config dir; the control socket ignored it and sat in $XDG_RUNTIME_DIR/tty7 or ~/.local/share/tty7 — under the same basename, `daemon.sock`. So they were told apart by directory alone, and the CLI, handed one path in TTY7_SOCKET, reconstructed the other with with_file_name: on the default layout that returns the input unchanged. Pane verbs dialed the control socket and the daemon hung up on them. A --config-dir server was worse: it published the *default* control socket to the shells it spawned, so a CLI inside an isolated instance drove a different server. The e2e suite passed throughout because its harness set TTY7_CONTROL_SOCK explicitly, placing both endpoints in one directory under different names — a layout production never produces. It had removed the bug's precondition. Now: the control socket is derived from the config dir like the pane socket (control.sock beside daemon.sock, mirroring Windows' control.port/daemon.port, with -control on the hashed fallback so the two cannot collide), and panes are handed TTY7_CONFIG_DIR instead of a socket path. A CLI inherits it, so ControlClient::connect and PaneClient::local resolve the same two sockets the server opened, through the same functions. No second derivation to disagree. remote_link's remote_control_socket was a third copy of the old rule, used to locate a remote server's endpoint before connecting; it follows the config dir too, and the env probe now reads $TTY7_CONFIG_DIR. Drops the CLI's server-lifecycle guard: stop/start already follow the config dir through transport::connect and --config-dir, so there is no longer a mismatch to refuse. The e2e case that covered only `status` over a lone variable now also runs a pane verb — the asymmetry it missed is exactly what broke. Note: this moves the control socket for existing installs. A running pre-change daemon will not be found at the new path, which is the honest outcome — its control dialect is v3 against this build's v4, so reaching it only produced a version error anyway.
155 lines
5.7 KiB
Rust
155 lines
5.7 KiB
Rust
#![cfg(unix)]
|
|
|
|
use std::io::{BufRead, BufReader};
|
|
use std::os::unix::net::{UnixListener, UnixStream};
|
|
use std::process::{Command, Stdio};
|
|
|
|
use tty7_core::daemon::control::ControlHello;
|
|
use tty7_core::daemon::remote_link::{RemoteEnv, remote_control_socket};
|
|
use tty7_core::daemon::router::{RemoteRouter, RouteAck, RouteHeader};
|
|
use tty7_core::host::Host;
|
|
use tty7_core::host::remote::RemoteHost;
|
|
|
|
const EXE: &str = env!("CARGO_BIN_EXE_tty7-server");
|
|
|
|
#[test]
|
|
fn a_routed_connection_reaches_a_real_server() {
|
|
let dir = tempfile::TempDir::new().unwrap();
|
|
let hub = dir.path().join("hub.sock");
|
|
let listener = UnixListener::bind(&hub).unwrap();
|
|
|
|
let router = std::thread::spawn(move || {
|
|
let (stream, _) = listener.accept().unwrap();
|
|
let mut reader = stream.try_clone().unwrap();
|
|
let (kind, payload) = tty7_core::daemon::protocol::read_frame(&mut reader).unwrap();
|
|
assert_eq!(kind, tty7_core::daemon::router::ROUTE_KIND);
|
|
let header = RouteHeader::decode(&payload).unwrap();
|
|
RemoteRouter::route(stream, &header)
|
|
});
|
|
|
|
let mut sock = UnixStream::connect(&hub).unwrap();
|
|
let missing = dir.path().join("nobody-here.sock");
|
|
let header = RouteHeader::local_stdio(
|
|
EXE,
|
|
&[
|
|
"--stdio",
|
|
"--serve",
|
|
"--control-sock",
|
|
&missing.to_string_lossy(),
|
|
],
|
|
);
|
|
header.write(&mut sock).unwrap();
|
|
|
|
let ack = RouteAck::read(&mut sock).expect("the route should be accepted");
|
|
assert_eq!(ack.link.as_deref(), Some("local-stdio"));
|
|
|
|
let hello = ControlHello::host_rpc("router-test", "localhost");
|
|
let host = RemoteHost::over_unix(sock, "routed:local-stdio", &hello)
|
|
.expect("handshake through the router");
|
|
|
|
let sandbox = tempfile::TempDir::new().unwrap();
|
|
let file = host.join(sandbox.path(), "through-the-router.txt");
|
|
host.write_file(&file, b"two hops and a pipe").unwrap();
|
|
assert_eq!(host.read_file(&file, 1024).unwrap(), b"two hops and a pipe");
|
|
|
|
let big = host.join(sandbox.path(), "big.bin");
|
|
let body: Vec<u8> = (0..2 * 1024 * 1024u32).map(|i| (i % 251) as u8).collect();
|
|
host.write_file(&big, &body).unwrap();
|
|
assert!(host.read_file(&big, 8 * 1024 * 1024).unwrap() == body);
|
|
|
|
assert_eq!(host.read_dir(sandbox.path(), None).unwrap().len(), 2);
|
|
|
|
drop(host);
|
|
let _ = router.join().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn an_unreachable_target_is_reported_not_dropped() {
|
|
let dir = tempfile::TempDir::new().unwrap();
|
|
let hub = dir.path().join("hub.sock");
|
|
let listener = UnixListener::bind(&hub).unwrap();
|
|
|
|
let router = std::thread::spawn(move || {
|
|
let (stream, _) = listener.accept().unwrap();
|
|
let mut reader = stream.try_clone().unwrap();
|
|
let (_, payload) = tty7_core::daemon::protocol::read_frame(&mut reader).unwrap();
|
|
let header = RouteHeader::decode(&payload).unwrap();
|
|
RemoteRouter::route(stream, &header)
|
|
});
|
|
|
|
let mut sock = UnixStream::connect(&hub).unwrap();
|
|
RouteHeader::local_stdio("tty7-server-that-does-not-exist", &[])
|
|
.write(&mut sock)
|
|
.unwrap();
|
|
|
|
let err = RouteAck::read(&mut sock).expect_err("there is nothing to route to");
|
|
assert!(
|
|
!err.to_string().is_empty(),
|
|
"the failure must say something"
|
|
);
|
|
assert!(router.join().unwrap().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn the_derived_remote_socket_is_the_one_the_server_binds() {
|
|
let with_runtime = tempfile::TempDir::new().unwrap();
|
|
let home = tempfile::TempDir::new().unwrap();
|
|
let runtime_path = with_runtime.path().to_string_lossy().to_string();
|
|
let home_path = home.path().to_string_lossy().to_string();
|
|
|
|
let bound = bound_control_socket(Some(&runtime_path), &home_path);
|
|
let derived = remote_control_socket(&RemoteEnv {
|
|
control_sock: None,
|
|
config_dir: None,
|
|
xdg_runtime_dir: Some(runtime_path.clone()),
|
|
home: Some(home_path.clone()),
|
|
tmpdir: std::env::var("TMPDIR").ok(),
|
|
});
|
|
assert_eq!(derived.as_deref(), Some(bound.as_str()));
|
|
|
|
let bound = bound_control_socket(None, &home_path);
|
|
let derived = remote_control_socket(&RemoteEnv {
|
|
control_sock: None,
|
|
config_dir: None,
|
|
xdg_runtime_dir: None,
|
|
home: Some(home_path.clone()),
|
|
tmpdir: std::env::var("TMPDIR").ok(),
|
|
});
|
|
assert_eq!(derived.as_deref(), Some(bound.as_str()));
|
|
}
|
|
|
|
fn bound_control_socket(runtime_dir: Option<&str>, home: &str) -> String {
|
|
// Deliberately no --config-dir: the control socket is derived from the
|
|
// config dir, and remote_control_socket derives the remote's from $HOME the
|
|
// same way. Overriding it here would compare two different rules. HOME is
|
|
// already a temp dir, so this stays isolated.
|
|
let mut cmd = Command::new(EXE);
|
|
cmd.arg("--daemon")
|
|
.env("HOME", home)
|
|
.env_remove("TTY7_CONFIG_DIR")
|
|
.env_remove("TTY7_CONTROL_SOCK")
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::piped());
|
|
match runtime_dir {
|
|
Some(dir) => cmd.env("XDG_RUNTIME_DIR", dir),
|
|
None => cmd.env_remove("XDG_RUNTIME_DIR"),
|
|
};
|
|
let mut child = cmd.spawn().expect("start tty7-server --daemon");
|
|
|
|
let stderr = BufReader::new(child.stderr.take().expect("piped"));
|
|
let mut bound = None;
|
|
for line in stderr.lines().map_while(Result::ok) {
|
|
if let Some(path) = line.strip_prefix("tty7-server: control socket at ") {
|
|
bound = Some(path.to_string());
|
|
break;
|
|
}
|
|
assert!(
|
|
!line.contains("control listener unavailable"),
|
|
"the server could not bind at all: {line}"
|
|
);
|
|
}
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
bound.expect("the server prints the control socket it bound")
|
|
}
|