diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-01 19:30:58 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-01 19:32:26 +0300 |
| commit | ca4270b31fe93c180f7321cb4006cb5e7307d889 (patch) | |
| tree | 72628e311a1b8e5ebf78c74cbc61d990253dc4c4 /tests | |
| parent | fb0c9717cfa961a47d63272e622e9e35a001082a (diff) | |
feat: add authoritative pi hook integration
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/api_ping.rs | 371 | ||||
| -rw-r--r-- | tests/cli_wrapper.rs | 49 |
2 files changed, 420 insertions, 0 deletions
diff --git a/tests/api_ping.rs b/tests/api_ping.rs index 17cda558..6b7cbf24 100644 --- a/tests/api_ping.rs +++ b/tests/api_ping.rs @@ -479,6 +479,377 @@ fn events_subscribe_streams_lifecycle_and_agent_events() { } #[cfg(not(target_os = "macos"))] +#[cfg(not(target_os = "macos"))] +#[test] +fn pane_report_agent_updates_effective_state() { + 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 bin_dir = base.join("bin"); + + fs::create_dir_all(&bin_dir).unwrap(); + let fake_pi = bin_dir.join("pi"); + fs::write(&fake_pi, "#!/bin/sh\nprintf 'Working...\\n'\nsleep 3\n").unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&fake_pi).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&fake_pi, perms).unwrap(); + } + + let inherited_path = std::env::var("PATH").unwrap_or_default(); + let path_override = format!("{}:{}", bin_dir.display(), inherited_path); + let child = spawn_herdr_with_path( + &config_home, + &runtime_dir, + &socket_path, + Some(Path::new(&path_override)), + ); + wait_for_socket(&socket_path, Duration::from_secs(5)); + + let created = send_request( + &socket_path, + &format!( + r#"{{"id":"req_hook_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 send_pi = send_request( + &socket_path, + &format!( + r#"{{"id":"req_hook_2","method":"pane.send_text","params":{{"pane_id":"{}","text":"pi"}}}}"#, + pane_id + ), + ); + assert_eq!(send_pi["result"]["type"], "ok"); + let send_enter = send_request( + &socket_path, + &format!( + r#"{{"id":"req_hook_3","method":"pane.send_keys","params":{{"pane_id":"{}","keys":["Enter"]}}}}"#, + pane_id + ), + ); + assert_eq!(send_enter["result"]["type"], "ok"); + + let deadline = Instant::now() + Duration::from_secs(3); + loop { + let pane = send_request( + &socket_path, + &format!( + r#"{{"id":"req_hook_detect","method":"pane.get","params":{{"pane_id":"{}"}}}}"#, + pane_id + ), + ); + if pane["result"]["pane"]["agent"] == "pi" { + break; + } + assert!( + Instant::now() < deadline, + "pi agent was never detected: {pane}" + ); + thread::sleep(Duration::from_millis(100)); + } + + let hook = send_request( + &socket_path, + &format!( + r#"{{"id":"req_hook_5","method":"pane.report_agent","params":{{"pane_id":"{}","source":"herdr:pi","agent":"pi","state":"working","message":"thinking"}}}}"#, + pane_id + ), + ); + assert_eq!(hook["result"]["type"], "ok"); + + let pane = send_request( + &socket_path, + &format!( + r#"{{"id":"req_hook_6","method":"pane.get","params":{{"pane_id":"{}"}}}}"#, + pane_id + ), + ); + assert_eq!(pane["result"]["pane"]["agent"], "pi"); + assert_eq!(pane["result"]["pane"]["agent_state"], "working"); + + cleanup_spawned_herdr(child, base); +} + +#[cfg(not(target_os = "macos"))] +#[test] +fn pane_release_agent_suppresses_reacquire_during_graceful_exit() { + 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 bin_dir = base.join("bin"); + + fs::create_dir_all(&bin_dir).unwrap(); + let fake_pi = bin_dir.join("pi"); + let stop_file = base.join("pi-stop"); + fs::write( + &fake_pi, + format!( + "#!/bin/sh\nprintf 'Working...\\n'\nwhile [ ! -f '{}' ]; do sleep 0.05; done\n", + stop_file.display() + ), + ) + .unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&fake_pi).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&fake_pi, perms).unwrap(); + } + + let inherited_path = std::env::var("PATH").unwrap_or_default(); + let path_override = format!("{}:{}", bin_dir.display(), inherited_path); + let child = spawn_herdr_with_path( + &config_home, + &runtime_dir, + &socket_path, + Some(Path::new(&path_override)), + ); + wait_for_socket(&socket_path, Duration::from_secs(5)); + + let created = send_request( + &socket_path, + &format!( + r#"{{"id":"req_release_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 send_pi = send_request( + &socket_path, + &format!( + r#"{{"id":"req_release_2","method":"pane.send_text","params":{{"pane_id":"{}","text":"pi"}}}}"#, + pane_id + ), + ); + assert_eq!(send_pi["result"]["type"], "ok"); + let send_enter = send_request( + &socket_path, + &format!( + r#"{{"id":"req_release_3","method":"pane.send_keys","params":{{"pane_id":"{}","keys":["Enter"]}}}}"#, + pane_id + ), + ); + assert_eq!(send_enter["result"]["type"], "ok"); + + let deadline = Instant::now() + Duration::from_secs(3); + loop { + let pane = send_request( + &socket_path, + &format!( + r#"{{"id":"req_release_detect","method":"pane.get","params":{{"pane_id":"{}"}}}}"#, + pane_id + ), + ); + if pane["result"]["pane"]["agent"] == "pi" { + break; + } + assert!( + Instant::now() < deadline, + "pi agent was never detected: {pane}" + ); + thread::sleep(Duration::from_millis(100)); + } + + let hook = send_request( + &socket_path, + &format!( + r#"{{"id":"req_release_4","method":"pane.report_agent","params":{{"pane_id":"{}","source":"herdr:pi","agent":"pi","state":"working"}}}}"#, + pane_id + ), + ); + assert_eq!(hook["result"]["type"], "ok"); + + let released = send_request( + &socket_path, + &format!( + r#"{{"id":"req_release_5","method":"pane.release_agent","params":{{"pane_id":"{}","source":"herdr:pi","agent":"pi"}}}}"#, + pane_id + ), + ); + assert_eq!(released["result"]["type"], "ok"); + + let suppression_deadline = Instant::now() + Duration::from_millis(300); + while Instant::now() < suppression_deadline { + let pane = send_request( + &socket_path, + &format!( + r#"{{"id":"req_release_6","method":"pane.get","params":{{"pane_id":"{}"}}}}"#, + pane_id + ), + ); + assert!( + pane["result"]["pane"]["agent"].is_null(), + "pane reacquired pi during graceful release: {pane}" + ); + assert_eq!(pane["result"]["pane"]["agent_state"], "unknown"); + thread::sleep(Duration::from_millis(50)); + } + + fs::write(&stop_file, "stop").unwrap(); + + let cleared_deadline = Instant::now() + Duration::from_secs(1); + loop { + let pane = send_request( + &socket_path, + &format!( + r#"{{"id":"req_release_7","method":"pane.get","params":{{"pane_id":"{}"}}}}"#, + pane_id + ), + ); + if pane["result"]["pane"]["agent"].is_null() + && pane["result"]["pane"]["agent_state"] == "unknown" + { + break; + } + assert!( + Instant::now() < cleared_deadline, + "pi agent was not cleared promptly after release: {pane}" + ); + thread::sleep(Duration::from_millis(50)); + } + + cleanup_spawned_herdr(child, base); +} + +#[cfg(not(target_os = "macos"))] +#[test] +fn pane_clear_agent_authority_restores_fallback_state() { + 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 bin_dir = base.join("bin"); + + fs::create_dir_all(&bin_dir).unwrap(); + let fake_pi = bin_dir.join("pi"); + fs::write(&fake_pi, "#!/bin/sh\nprintf 'Working...\\n'\nsleep 3\n").unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&fake_pi).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&fake_pi, perms).unwrap(); + } + + let inherited_path = std::env::var("PATH").unwrap_or_default(); + let path_override = format!("{}:{}", bin_dir.display(), inherited_path); + let child = spawn_herdr_with_path( + &config_home, + &runtime_dir, + &socket_path, + Some(Path::new(&path_override)), + ); + wait_for_socket(&socket_path, Duration::from_secs(5)); + + let created = send_request( + &socket_path, + &format!( + r#"{{"id":"req_clear_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 send_pi = send_request( + &socket_path, + &format!( + r#"{{"id":"req_clear_2","method":"pane.send_text","params":{{"pane_id":"{}","text":"pi"}}}}"#, + pane_id + ), + ); + assert_eq!(send_pi["result"]["type"], "ok"); + let send_enter = send_request( + &socket_path, + &format!( + r#"{{"id":"req_clear_3","method":"pane.send_keys","params":{{"pane_id":"{}","keys":["Enter"]}}}}"#, + pane_id + ), + ); + assert_eq!(send_enter["result"]["type"], "ok"); + + let deadline = Instant::now() + Duration::from_secs(3); + loop { + let pane = send_request( + &socket_path, + &format!( + r#"{{"id":"req_clear_detect","method":"pane.get","params":{{"pane_id":"{}"}}}}"#, + pane_id + ), + ); + if pane["result"]["pane"]["agent"] == "pi" { + break; + } + assert!( + Instant::now() < deadline, + "pi agent was never detected: {pane}" + ); + thread::sleep(Duration::from_millis(100)); + } + + let fallback_before_hook = send_request( + &socket_path, + &format!( + r#"{{"id":"req_clear_fallback","method":"pane.get","params":{{"pane_id":"{}"}}}}"#, + pane_id + ), + ); + let fallback_state = fallback_before_hook["result"]["pane"]["agent_state"] + .as_str() + .unwrap() + .to_string(); + + let hook = send_request( + &socket_path, + &format!( + r#"{{"id":"req_clear_4","method":"pane.report_agent","params":{{"pane_id":"{}","source":"herdr:pi","agent":"pi","state":"idle"}}}}"#, + pane_id + ), + ); + assert_eq!(hook["result"]["type"], "ok"); + + let cleared = send_request( + &socket_path, + &format!( + r#"{{"id":"req_clear_5","method":"pane.clear_agent_authority","params":{{"pane_id":"{}","source":"herdr:pi"}}}}"#, + pane_id + ), + ); + assert_eq!(cleared["result"]["type"], "ok"); + + let pane = send_request( + &socket_path, + &format!( + r#"{{"id":"req_clear_6","method":"pane.get","params":{{"pane_id":"{}"}}}}"#, + pane_id + ), + ); + assert_eq!(pane["result"]["pane"]["agent"], "pi"); + assert_eq!(pane["result"]["pane"]["agent_state"], fallback_state); + + cleanup_spawned_herdr(child, base); +} + #[test] fn events_subscribe_streams_output_and_agent_state_events() { let _lock = test_lock(); diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs index 7f37897b..e371eca4 100644 --- a/tests/cli_wrapper.rs +++ b/tests/cli_wrapper.rs @@ -476,6 +476,55 @@ fn ids_are_compact_and_positional() { } #[test] +fn pane_shell_gets_herdr_socket_and_pane_env() { + 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 herdr = 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_env_1","method":"workspace.create","params":{{"cwd":"{}","focus":true}}}}"#, + base.display() + ), + ); + assert_eq!(created["result"]["workspace"]["workspace_id"], "1"); + + let env_capture = base.join("pane-env.txt"); + let ran = run_cli( + &socket_path, + &[ + "pane", + "run", + "1-1", + &format!( + "printf '%s\\n%s\\n' \"$HERDR_SOCKET_PATH\" \"$HERDR_PANE_ID\" > {}", + env_capture.display() + ), + ], + ); + assert!(ran.status.success()); + + let deadline = Instant::now() + Duration::from_secs(3); + while Instant::now() < deadline && !env_capture.exists() { + thread::sleep(Duration::from_millis(25)); + } + assert!(env_capture.exists(), "env capture file was not created"); + let text = fs::read_to_string(&env_capture).unwrap(); + assert!( + text.contains(&socket_path.display().to_string()), + "env file was: {text:?}" + ); + assert!(text.contains("p_"), "env file was: {text:?}"); + + cleanup_spawned_herdr(herdr, base); +} + +#[test] fn wait_agent_state_exits_when_state_matches() { let base = unique_test_dir(); let config_home = base.join("config"); |
