aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-26 17:10:28 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-26 17:10:28 +0300
commit3750e0bae21c54da3947d337584f180a21d0bc67 (patch)
tree4542d8af76b4fee0c428ca5e9378a06946ef62ca /tests
parent1b980eaa9d4cbc050b6b81525e4b6b72ae853be5 (diff)
feat: add pane metadata reporting
refs #36
Diffstat (limited to 'tests')
-rw-r--r--tests/api_ping.rs190
-rw-r--r--tests/cli_wrapper.rs131
2 files changed, 318 insertions, 3 deletions
diff --git a/tests/api_ping.rs b/tests/api_ping.rs
index a4d07360..31443317 100644
--- a/tests/api_ping.rs
+++ b/tests/api_ping.rs
@@ -141,16 +141,24 @@ impl JsonLineReader {
}
fn read_json_line(&mut self, timeout: Duration) -> serde_json::Value {
+ self.try_read_json_line(timeout)
+ .unwrap_or_else(|| panic!("timed out waiting for json line"))
+ }
+
+ fn try_read_json_line(&mut self, timeout: Duration) -> Option<serde_json::Value> {
let deadline = Instant::now() + timeout;
self.stream.set_nonblocking(true).unwrap();
loop {
- assert!(Instant::now() < deadline, "timed out waiting for json line");
+ if Instant::now() >= deadline {
+ self.stream.set_nonblocking(false).unwrap();
+ return None;
+ }
if let Some(pos) = self.buf.iter().position(|&b| b == b'\n') {
let line = String::from_utf8(self.buf.drain(..=pos).collect()).unwrap();
self.stream.set_nonblocking(false).unwrap();
- return serde_json::from_str(&line).unwrap();
+ return Some(serde_json::from_str(&line).unwrap());
}
let mut bytes = [0u8; 256];
@@ -1220,6 +1228,93 @@ fn pane_report_agent_updates_effective_state() {
assert_eq!(pane["result"]["pane"]["agent"], "pi");
assert_eq!(pane["result"]["pane"]["agent_status"], "working");
+ let metadata = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"req_hook_metadata","method":"pane.report_metadata","params":{{"pane_id":"{}","source":"user:pi-display","agent":"pi","applies_to_source":"herdr:pi","title":"Refactor auth","display_agent":"Pi auth","custom_status":"middleware","state_labels":{{"working":"deep in the mines"}}}}}}"#,
+ pane_id
+ ),
+ );
+ assert_eq!(metadata["result"]["type"], "ok");
+
+ let pane = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"req_hook_metadata_get","method":"pane.get","params":{{"pane_id":"{}"}}}}"#,
+ pane_id
+ ),
+ );
+ assert_eq!(pane["result"]["pane"]["agent"], "pi");
+ assert_eq!(pane["result"]["pane"]["agent_status"], "working");
+ assert_eq!(pane["result"]["pane"]["title"], "Refactor auth");
+ assert_eq!(pane["result"]["pane"]["display_agent"], "Pi auth");
+ assert_eq!(pane["result"]["pane"]["custom_status"], "middleware");
+ assert_eq!(
+ pane["result"]["pane"]["state_labels"]["working"],
+ "deep in the mines"
+ );
+
+ let agent = send_request(
+ &socket_path,
+ r#"{"id":"req_hook_metadata_agent","method":"agent.get","params":{"target":"pi"}}"#,
+ );
+ assert_eq!(agent["result"]["agent"]["agent"], "pi");
+ assert_eq!(agent["result"]["agent"]["title"], "Refactor auth");
+ assert_eq!(agent["result"]["agent"]["display_agent"], "Pi auth");
+ assert_eq!(agent["result"]["agent"]["custom_status"], "middleware");
+ assert_eq!(
+ agent["result"]["agent"]["state_labels"]["working"],
+ "deep in the mines"
+ );
+
+ let invalid_metadata = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"req_hook_metadata_invalid","method":"pane.report_metadata","params":{{"pane_id":"{}","source":"user:pi-display","custom_status":"x","clear_custom_status":true}}}}"#,
+ pane_id
+ ),
+ );
+ assert_eq!(
+ invalid_metadata["error"]["code"],
+ "invalid_metadata_request"
+ );
+
+ let blank_source_metadata = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"req_hook_metadata_blank_source","method":"pane.report_metadata","params":{{"pane_id":"{}","source":" ","custom_status":"x"}}}}"#,
+ pane_id
+ ),
+ );
+ assert_eq!(
+ blank_source_metadata["error"]["code"],
+ "invalid_metadata_request"
+ );
+
+ let blank_title_clear_metadata = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"req_hook_metadata_blank_title_clear","method":"pane.report_metadata","params":{{"pane_id":"{}","source":"user:pi-display","title":" ","clear_title":true}}}}"#,
+ pane_id
+ ),
+ );
+ assert_eq!(
+ blank_title_clear_metadata["error"]["code"],
+ "invalid_metadata_request"
+ );
+
+ let blank_authority_source_metadata = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"req_hook_metadata_blank_authority_source","method":"pane.report_metadata","params":{{"pane_id":"{}","source":"user:pi-display","applies_to_source":" ","custom_status":"x"}}}}"#,
+ pane_id
+ ),
+ );
+ assert_eq!(
+ blank_authority_source_metadata["error"]["code"],
+ "invalid_metadata_request"
+ );
+
cleanup_spawned_herdr(child, base);
}
@@ -1803,3 +1898,94 @@ fn pane_info_and_subscriptions_expose_done_agent_status() {
cleanup_spawned_herdr(child, base);
}
+
+#[test]
+fn metadata_status_subscription_filter_and_ttl_expiry_are_observable() {
+ let _lock = test_lock();
+ let base = unique_test_dir();
+ let config_home = base.join("config");
+ let runtime_dir = base.join("runtime");
+ let socket_path = runtime_dir.join("herdr.sock");
+
+ let child = spawn_herdr(&config_home, &runtime_dir, &socket_path);
+ wait_for_socket(&socket_path, Duration::from_secs(5));
+
+ let created = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"req_meta_sub_1","method":"workspace.create","params":{{"cwd":"{}","focus":true}}}}"#,
+ base.display()
+ ),
+ );
+ let pane_id = created["result"]["workspace"]["workspace_id"]
+ .as_str()
+ .map(|workspace_id| format!("{}-1", workspace_id))
+ .unwrap();
+
+ let report_agent = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"req_meta_sub_2","method":"pane.report_agent","params":{{"pane_id":"{}","source":"herdr:pi","agent":"pi","state":"working"}}}}"#,
+ pane_id
+ ),
+ );
+ assert_eq!(report_agent["result"]["type"], "ok");
+
+ let mut done_reader = open_subscription(
+ &socket_path,
+ &format!(
+ r#"{{"id":"sub_meta_done","method":"events.subscribe","params":{{"subscriptions":[{{"type":"pane.agent_status_changed","pane_id":"{}","agent_status":"done"}}]}}}}"#,
+ pane_id,
+ ),
+ );
+ let ack = done_reader.read_json_line(Duration::from_secs(2));
+ assert_eq!(ack["id"], "sub_meta_done");
+ assert_eq!(ack["result"]["type"], "subscription_started");
+
+ let metadata = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"req_meta_sub_3","method":"pane.report_metadata","params":{{"pane_id":"{}","source":"user:pi-display","agent":"pi","applies_to_source":"herdr:pi","custom_status":"filtered out"}}}}"#,
+ pane_id
+ ),
+ );
+ assert_eq!(metadata["result"]["type"], "ok");
+ assert!(
+ done_reader
+ .try_read_json_line(Duration::from_millis(500))
+ .is_none(),
+ "done-filtered subscription emitted for a working metadata-only change"
+ );
+
+ let mut reader = open_subscription(
+ &socket_path,
+ &format!(
+ r#"{{"id":"sub_meta_ttl","method":"events.subscribe","params":{{"subscriptions":[{{"type":"pane.agent_status_changed","pane_id":"{}"}}]}}}}"#,
+ pane_id,
+ ),
+ );
+ let ack = reader.read_json_line(Duration::from_secs(2));
+ assert_eq!(ack["id"], "sub_meta_ttl");
+ assert_eq!(ack["result"]["type"], "subscription_started");
+
+ let metadata = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"req_meta_sub_4","method":"pane.report_metadata","params":{{"pane_id":"{}","source":"user:pi-display","agent":"pi","applies_to_source":"herdr:pi","custom_status":"short lived","ttl_ms":100}}}}"#,
+ pane_id
+ ),
+ );
+ assert_eq!(metadata["result"]["type"], "ok");
+
+ let set_event = reader.read_json_line(Duration::from_secs(2));
+ assert_eq!(set_event["event"], "pane.agent_status_changed");
+ assert_eq!(set_event["data"]["agent_status"], "working");
+ assert_eq!(set_event["data"]["custom_status"], "short lived");
+
+ let expiry_event = reader.read_json_line(Duration::from_secs(3));
+ assert_eq!(expiry_event["event"], "pane.agent_status_changed");
+ assert_eq!(expiry_event["data"]["agent_status"], "working");
+ assert!(expiry_event["data"]["custom_status"].is_null());
+
+ cleanup_spawned_herdr(child, base);
+}
diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs
index 4a281c79..0482fd1c 100644
--- a/tests/cli_wrapper.rs
+++ b/tests/cli_wrapper.rs
@@ -693,6 +693,135 @@ fn pane_run_sends_one_send_input_request_with_enter_key() {
}
#[test]
+fn pane_report_metadata_sends_presentation_request() {
+ let base = unique_test_dir();
+ fs::create_dir_all(&base).unwrap();
+ let socket_path = base.join("herdr.sock");
+ let listener = UnixListener::bind(&socket_path).unwrap();
+
+ let server = thread::spawn(move || {
+ let (mut stream, _) = listener.accept().unwrap();
+ let mut line = String::new();
+ let mut reader = BufReader::new(stream.try_clone().unwrap());
+ reader.read_line(&mut line).unwrap();
+ stream
+ .write_all(br#"{"id":"cli:request","result":{"type":"ok"}}"#)
+ .unwrap();
+ stream.write_all(b"\n").unwrap();
+ stream.flush().unwrap();
+ line
+ });
+
+ let run = run_cli(
+ &socket_path,
+ &[
+ "pane",
+ "report-metadata",
+ "1-1",
+ "--source",
+ "user:claude-title",
+ "--agent",
+ "claude",
+ "--applies-to-source",
+ "herdr:claude",
+ "--title",
+ "Refactor auth",
+ "--display-agent",
+ "Claude auth",
+ "--custom-status",
+ "middleware",
+ "--state-label",
+ "working=deep in the mines",
+ "--ttl-ms",
+ "3600000",
+ ],
+ );
+ assert!(
+ run.status.success(),
+ "stderr: {}",
+ String::from_utf8_lossy(&run.stderr)
+ );
+
+ let line = server.join().unwrap();
+ let request: serde_json::Value = serde_json::from_str(&line).unwrap();
+ assert_eq!(request["method"], "pane.report_metadata");
+ assert_eq!(request["params"]["pane_id"], "1-1");
+ assert_eq!(request["params"]["source"], "user:claude-title");
+ assert_eq!(request["params"]["agent"], "claude");
+ assert_eq!(request["params"]["applies_to_source"], "herdr:claude");
+ assert_eq!(request["params"]["title"], "Refactor auth");
+ assert_eq!(request["params"]["display_agent"], "Claude auth");
+ assert_eq!(request["params"]["custom_status"], "middleware");
+ assert_eq!(
+ request["params"]["state_labels"]["working"],
+ "deep in the mines"
+ );
+ assert_eq!(request["params"]["ttl_ms"], 3_600_000);
+
+ cleanup_test_base(&base);
+}
+
+#[test]
+fn pane_report_metadata_rejects_blank_source_before_socket_request() {
+ let base = unique_test_dir();
+ fs::create_dir_all(&base).unwrap();
+ let socket_path = base.join("missing.sock");
+
+ let run = run_cli(
+ &socket_path,
+ &[
+ "pane",
+ "report-metadata",
+ "1-1",
+ "--source",
+ " ",
+ "--custom-status",
+ "middleware",
+ ],
+ );
+
+ assert_eq!(run.status.code(), Some(2));
+ assert!(
+ String::from_utf8_lossy(&run.stderr).contains("missing required --source"),
+ "stderr: {}",
+ String::from_utf8_lossy(&run.stderr)
+ );
+
+ cleanup_test_base(&base);
+}
+
+#[test]
+fn pane_report_metadata_rejects_blank_applies_to_source_before_socket_request() {
+ let base = unique_test_dir();
+ fs::create_dir_all(&base).unwrap();
+ let socket_path = base.join("missing.sock");
+
+ let run = run_cli(
+ &socket_path,
+ &[
+ "pane",
+ "report-metadata",
+ "1-1",
+ "--source",
+ "user:claude-title",
+ "--applies-to-source",
+ " ",
+ "--custom-status",
+ "middleware",
+ ],
+ );
+
+ assert_eq!(run.status.code(), Some(2));
+ assert!(
+ String::from_utf8_lossy(&run.stderr).contains("missing value for --applies-to-source"),
+ "stderr: {}",
+ String::from_utf8_lossy(&run.stderr)
+ );
+
+ cleanup_test_base(&base);
+}
+
+#[test]
fn help_commands_exit_successfully() {
let help_cases: &[&[&str]] = &[
&["-h"],
@@ -1013,7 +1142,7 @@ fn integration_commands_run_locally_when_server_is_missing() {
.unwrap();
assert_eq!(integration_status.status.code(), Some(0));
let status_stdout = String::from_utf8_lossy(&integration_status.stdout);
- assert!(status_stdout.contains("pi: current (v2)"));
+ assert!(status_stdout.contains("pi: current (v3)"));
assert!(status_stdout.contains("claude: not installed"));
let integration_uninstall = Command::new(env!("CARGO_BIN_EXE_herdr"))