use super::*;
#[test]
fn workspace_labels_keep_meaningful_words_and_boundaries() {
for (title, expected) in [
(
"Audit the Value of Output Compression",
Some("audit-value-output-compression"),
),
("Fix: THE parser / not the API!", Some("fix-parser-not-api")),
("The of", Some("the-of")),
("---", None),
("Réparer le Café", Some("réparer-le-café")),
] {
assert_eq!(workspace_title_slug(title).as_deref(), expected);
}
assert_eq!(
workspace_title_slug(&format!("{} next", "x".repeat(63))).unwrap(),
"x".repeat(63)
);
}
struct WorkspaceTransport {
lines: Mutex<Vec<serde_json::Value>>,
response: serde_json::Value,
}
impl HerdrTransport for WorkspaceTransport {
fn write_line(&self, _: &Path, line: &str) -> io::Result<()> {
self.lines.lock().unwrap().push(serde_json::from_str(line)?);
Ok(())
}
fn request_line(&self, _: &Path, line: &str) -> io::Result<String> {
let request: serde_json::Value = serde_json::from_str(line)?;
let mut response = self.response.clone();
response["id"] = request["id"].clone();
self.lines.lock().unwrap().push(request);
Ok(response.to_string())
}
}
#[test]
fn generated_title_renames_the_callers_resolved_workspace() {
let transport = Arc::new(WorkspaceTransport {
lines: Mutex::default(),
response: serde_json::json!({"result": {"pane": {"workspace_id": "wMoved"}}}),
});
let owner =
HerdrOwner::new_with_transport("pane-caller".into(), "/unused".into(), transport.clone());
let reporter = owner.reporter();
reporter.report_agent_session("session-a");
transport.lines.lock().unwrap().clear();
reporter.rename_workspace_from_generated_title(
"session-a",
"Audit the Value of Output Compression",
);
let lines = transport.lines.lock().unwrap();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0]["method"], "pane.current");
assert_eq!(
lines[0]["params"],
serde_json::json!({"caller_pane_id": "pane-caller"})
);
assert_eq!(lines[1]["method"], "workspace.rename");
assert_eq!(
lines[1]["params"],
serde_json::json!({"workspace_id": "wMoved", "label": "audit-value-output-compression"})
);
}
#[test]
fn unresolved_workspace_never_falls_back_to_global_focus() {
for response in [
serde_json::json!({"result": {"pane": null}}),
serde_json::json!({"error": {"message": "missing pane"}}),
serde_json::json!({"result": {"pane": {"workspace_id": ""}}}),
] {
let transport = Arc::new(WorkspaceTransport {
lines: Mutex::default(),
response,
});
let owner = HerdrOwner::new_with_transport(
"pane-caller".into(),
"/unused".into(),
transport.clone(),
);
let reporter = owner.reporter();
reporter.report_agent_session("session-a");
transport.lines.lock().unwrap().clear();
reporter.rename_workspace_from_generated_title("session-a", "Fix parser");
assert_eq!(transport.lines.lock().unwrap().len(), 1);
}
}
#[test]
fn released_owner_prevents_workspace_rename() {
let transport = Arc::new(WorkspaceTransport {
lines: Mutex::default(),
response: serde_json::json!({"result": {"pane": {"workspace_id": "wMoved"}}}),
});
let owner =
HerdrOwner::new_with_transport("pane-caller".into(), "/unused".into(), transport.clone());
let reporter = owner.reporter();
reporter.report_agent_session("session-a");
owner.release();
transport.lines.lock().unwrap().clear();
reporter.rename_workspace_from_generated_title("session-a", "Fix parser");
assert!(transport.lines.lock().unwrap().is_empty());
}
#[test]
fn delayed_title_for_previous_session_skips_workspace_lookup() {
let transport = Arc::new(WorkspaceTransport {
lines: Mutex::default(),
response: serde_json::json!({"result": {"pane": {"workspace_id": "wMoved"}}}),
});
let owner =
HerdrOwner::new_with_transport("pane-caller".into(), "/unused".into(), transport.clone());
let reporter = owner.reporter();
reporter.report_agent_session_with_source("session-a", HerdrSessionStartSource::Startup);
reporter.report_agent_session_with_source("session-b", HerdrSessionStartSource::Select);
transport.lines.lock().unwrap().clear();
reporter.rename_workspace_from_generated_title("session-a", "Old title");
assert!(transport.lines.lock().unwrap().is_empty());
reporter.rename_workspace_from_generated_title("session-b", "Current title");
let lines = transport.lines.lock().unwrap();
assert_eq!(lines.len(), 2);
assert_eq!(lines[1]["params"]["label"], "current-title");
}
struct DelayedWorkspaceTransport {
recording: WorkspaceTransport,
lookup_started: crossbeam_channel::Sender<()>,
resume_lookup: crossbeam_channel::Receiver<()>,
}
impl HerdrTransport for DelayedWorkspaceTransport {
fn write_line(&self, path: &Path, line: &str) -> io::Result<()> {
self.recording.write_line(path, line)
}
fn request_line(&self, path: &Path, line: &str) -> io::Result<String> {
self.lookup_started.send(()).unwrap();
self.resume_lookup
.recv_timeout(std::time::Duration::from_secs(5))
.unwrap();
self.recording.request_line(path, line)
}
}
#[test]
fn session_switch_during_workspace_lookup_rejects_stale_rename() {
for return_to_a in [false, true] {
let (lookup_started, started) = crossbeam_channel::bounded(1);
let (resume, resume_lookup) = crossbeam_channel::bounded(1);
let transport = Arc::new(DelayedWorkspaceTransport {
recording: WorkspaceTransport {
lines: Mutex::default(),
response: serde_json::json!({"result": {"pane": {"workspace_id": "wMoved"}}}),
},
lookup_started,
resume_lookup,
});
let owner = HerdrOwner::new_with_transport(
"pane-caller".into(),
"/unused".into(),
transport.clone(),
);
let reporter = owner.reporter();
reporter.report_agent_session("session-a");
let worker_reporter = reporter.clone();
let worker = std::thread::spawn(move || {
worker_reporter.rename_workspace_from_generated_title("session-a", "Old title");
});
started
.recv_timeout(std::time::Duration::from_secs(5))
.unwrap();
reporter.report_agent_session_with_source("session-b", HerdrSessionStartSource::Select);
if return_to_a {
reporter.report_agent_session_with_source("session-a", HerdrSessionStartSource::Select);
}
transport.recording.lines.lock().unwrap().clear();
resume.send(()).unwrap();
worker.join().unwrap();
let lines = transport.recording.lines.lock().unwrap();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0]["method"], "pane.current");
}
}
#[cfg(unix)]
#[test]
fn workspace_lookup_reads_split_response_and_times_out_without_newline() {
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixListener;
for complete in [true, false] {
let directory = tempfile::TempDir::new().unwrap();
let socket = directory.path().join("herdr.sock");
let listener = UnixListener::bind(&socket).unwrap();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut request = String::new();
BufReader::new(&mut stream).read_line(&mut request).unwrap();
assert_eq!(request, "lookup\n");
stream.write_all(b"{\"result\":").unwrap();
if complete {
stream.write_all(b"null}\nignored").unwrap();
} else {
std::thread::sleep(Duration::from_millis(250));
}
});
let started = std::time::Instant::now();
let result = UnixSocketTransport.request_line(&socket, "lookup\n");
if complete {
assert_eq!(result.unwrap(), "{\"result\":null}");
} else {
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::TimedOut);
}
assert!(started.elapsed() < Duration::from_secs(1));
server.join().unwrap();
}
}