makepad/tools/agents/tests/names.rs

251 lines
9.2 KiB
Rust

#![cfg(unix)]
use makepad_agents::{protocol::SessionLocation, server, terminal::HostedTerminal};
use makepad_strict_json::{self as json, Value};
use std::{
fs,
io::{Read, Write},
os::unix::net::UnixStream,
path::PathBuf,
process::Command,
time::{Duration, Instant},
};
struct Session {
root: PathBuf,
state: PathBuf,
}
impl Drop for Session {
fn drop(&mut self) {
let _ = server::stop(&self.state, "name-test");
let _ = fs::remove_dir_all(&self.root);
}
}
fn send(stream: &mut UnixStream, kind: u8, payload: &[u8]) {
let mut frame = vec![kind];
frame.extend_from_slice(&(payload.len() as u32).to_be_bytes());
frame.extend_from_slice(payload);
stream.write_all(&frame).unwrap();
}
fn observe_title(stream: &mut UnixStream, terminal: &mut HostedTerminal, expected: &str) {
let deadline = Instant::now() + Duration::from_secs(5);
while terminal.title() != expected {
assert!(
Instant::now() < deadline,
"attached client did not receive {expected}"
);
let mut header = [0; 5];
stream.read_exact(&mut header).unwrap();
let length = u32::from_be_bytes(header[1..].try_into().unwrap()) as usize;
assert!(length <= 1024 * 1024);
let mut bytes = vec![0; length];
stream.read_exact(&mut bytes).unwrap();
assert_ne!(header[0], 5, "{}", String::from_utf8_lossy(&bytes));
if matches!(header[0], 2 | 3) {
terminal.process(&bytes);
}
}
}
#[test]
fn osc_and_current_session_cli_names_reach_list_and_attached_client() {
let root = std::env::temp_dir().join(format!(
"screen-names-{}-{}",
std::process::id(),
makepad_agents::protocol::random_token().unwrap()
));
fs::create_dir(&root).unwrap();
let root = root.canonicalize().unwrap();
let session = Session {
state: root.join("sessions"),
root,
};
let binary = env!("CARGO_BIN_EXE_agents");
let output = Command::new(binary)
.args(["start", "--state-dir"])
.arg(&session.state)
.args(["--session", "name-test", "--cwd"])
.arg(&session.root)
.args(["--", "/bin/sh"])
.env("SCREEN_TEST_BINARY", binary)
.output()
.unwrap();
assert!(
output.status.success(),
"{}; host log: {}",
String::from_utf8_lossy(&output.stderr),
fs::read_to_string(session.state.join("name-test.log")).unwrap_or_default()
);
let location = SessionLocation::open(&session.state, "name-test", false).unwrap();
let mut stream = UnixStream::connect(&location.socket_path).unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
send(
&mut stream,
1,
br#"{"version":1,"session_id":"name-test","cols":80,"rows":24,"read_only":false}"#,
);
let mut attached = HostedTerminal::new(80, 24, 100);
for (command, expected) in [
("printf '\\033]0;osc-zero\\007'\n", "osc-zero"),
("printf '\\033]2;osc-two\\033\\\\'\n", "osc-two"),
("\"$SCREEN_TEST_BINARY\" name cli-name\n", "cli-name"),
(
"\"$SCREEN_TEST_BINARY\" agents name wrapper-name\n",
"wrapper-name",
),
] {
send(&mut stream, 6, command.as_bytes());
observe_title(&mut stream, &mut attached, expected);
let output = Command::new(binary)
.args(["list", "--state-dir"])
.arg(&session.state)
.output()
.unwrap();
assert!(output.status.success());
let inventory = json::parse(&output.stdout).unwrap();
let row = &inventory.as_arr().unwrap()[0];
assert_eq!(row.get("title").and_then(Value::as_str), Some(expected));
assert_eq!(
row.get("session_id").and_then(Value::as_str),
Some("name-test")
);
assert_eq!(
row.get("cwd").and_then(Value::as_str),
session.root.to_str()
);
assert!(row.get("started_at_ms").and_then(Value::as_u64).unwrap() > 0);
let stored = json::parse(&fs::read(&location.metadata_path).unwrap()).unwrap();
assert_eq!(stored.get("title").and_then(Value::as_str), Some(expected));
}
drop(stream);
let stopped = server::stop(&session.state, "name-test").unwrap();
assert_eq!(
stopped.get("title").and_then(Value::as_str),
Some("wrapper-name")
);
assert_eq!(stopped.get("running").and_then(Value::as_bool), Some(false));
}
#[test]
fn shell_started_session_is_listed_with_osc_title_in_workspace_scope() {
let root = std::env::temp_dir().join(format!(
"screen-inventory-{}-{}",
std::process::id(),
makepad_agents::protocol::random_token().unwrap()
));
fs::create_dir(&root).unwrap();
let root = root.canonicalize().unwrap();
let session = Session {
state: root.join("local/agent_state/studio/iteration-verification/state/agent_sessions"),
root,
};
makepad_agents::protocol::private_directory(&session.state, true).unwrap();
let binary = env!("CARGO_BIN_EXE_agents");
// A shell starts the session before any Studio/client attaches. Omit cwd
// and state flags, as in tools/agents start --session NAME -- COMMAND.
let output = Command::new(binary)
.current_dir(&session.root)
.env_remove("MAKEPAD_AGENTS_STATE_DIR")
.env_remove("MAKEPAD_AGENTS_SESSION")
.args([
"start",
"--session",
"name-test",
"--",
"/bin/sh",
"-c",
"printf '\\033]0;hello-agent\\007'; exec sleep 600",
])
.output()
.unwrap();
assert!(
output.status.success(),
"{}; host log: {}",
String::from_utf8_lossy(&output.stderr),
fs::read_to_string(session.state.join("name-test.log")).unwrap_or_default()
);
for scope in ["workspace", "explicit", "inherited"] {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let mut list = Command::new(binary);
list.arg("list")
.current_dir(&session.root)
.env_remove("MAKEPAD_AGENTS_STATE_DIR");
match scope {
"explicit" => {
// Explicit scope wins over an unrelated inherited scope.
list.arg("--state-dir")
.arg(&session.state)
.env("MAKEPAD_AGENTS_STATE_DIR", session.root.join("empty"));
}
"inherited" => {
// Outside the workspace, the inherited scope still wins.
list.current_dir(&session.state)
.env("MAKEPAD_AGENTS_STATE_DIR", &session.state);
}
_ => {}
}
let output = list.output().unwrap();
assert!(
output.status.success(),
"{scope}: {}",
String::from_utf8_lossy(&output.stderr)
);
let inventory = json::parse(&output.stdout).unwrap();
let rows = inventory.as_arr().unwrap();
assert_eq!(rows.len(), 1, "{scope}: {inventory:?}");
let row = &rows[0];
assert_eq!(
row.get("session_id").and_then(Value::as_str),
Some("name-test")
);
assert_eq!(
row.get("state_dir").and_then(Value::as_str),
session.state.to_str()
);
assert_eq!(
row.get("cwd").and_then(Value::as_str),
session.root.to_str()
);
assert_eq!(row.get("running").and_then(Value::as_bool), Some(true));
assert_eq!(row.get("clients").and_then(Value::as_u64), Some(0));
if row.get("title").and_then(Value::as_str) == Some("hello-agent") {
break;
}
assert!(
Instant::now() < deadline,
"{scope}: OSC title missing: {row:?}"
);
std::thread::sleep(Duration::from_millis(20));
}
}
let stopped = server::stop(&session.state, "name-test").unwrap();
assert_eq!(stopped.get("running").and_then(Value::as_bool), Some(false));
assert!(server::list(&session.state)
.unwrap()
.as_arr()
.unwrap()
.is_empty());
}
#[test]
fn title_changes_project_to_existing_and_new_clients() {
use makepad_agents::snapshot::Projection;
let mut source = HostedTerminal::new(80, 24, 100);
let mut client = HostedTerminal::new(80, 24, 100);
let mut projection = Projection::default();
for (sequence, title) in [
(b"\x1b]0;zero\x07".as_slice(), "zero"),
(b"\x1b]2;two\x1b\\".as_slice(), "two"),
] {
source.process(sequence);
client.process(&source.render(&mut projection, 80, 24));
assert_eq!(client.title(), title);
}
source.set_title("same name");
client.process(&source.render(&mut projection, 80, 24));
assert_eq!(client.title(), "same name");
let mut new_client = HostedTerminal::new(100, 30, 100);
new_client.process(&source.render(&mut Projection::default(), 100, 30));
assert_eq!(new_client.title(), "same name");
}