diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 00:32:15 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 00:32:22 +0300 |
| commit | a22454f27ce096585e19d1787dba43f56d1505cf (patch) | |
| tree | bd90ad713146e710b59ff1532b9f867bc42cb07f | |
| parent | e9cbcf2f6f8654dd41b10826ef6aecd526f6b0b0 (diff) | |
fix: preserve plugins during live handoff
refs #893
| -rw-r--r-- | docs/next/CHANGELOG.md | 3 | ||||
| -rw-r--r-- | src/app/mod.rs | 1 | ||||
| -rw-r--r-- | tests/live_handoff.rs | 100 |
3 files changed, 104 insertions, 0 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 3ee3ae06..b15f009b 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Fixed +- Live handoff now preserves installed plugins and no longer lets the next plugin installation overwrite the existing registry. (#893) + ## [0.7.4] - 2026-07-15 ### Added diff --git a/src/app/mod.rs b/src/app/mod.rs index a77f2536..6ea84746 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -786,6 +786,7 @@ impl App { let pane_id_aliases = crate::persist::handoff_pane_aliases(snapshot, &workspaces); app.no_session = false; + app.state.installed_plugins = load_plugin_registry(app.no_session); let now = Instant::now(); if background_update_check_enabled(app.no_session, app.update_version_check_enabled) { app.next_auto_update_check = app diff --git a/tests/live_handoff.rs b/tests/live_handoff.rs index 6302ed68..b2fb9ef6 100644 --- a/tests/live_handoff.rs +++ b/tests/live_handoff.rs @@ -154,6 +154,7 @@ fn spawn_default_session_server(config_home: &Path, runtime_dir: &Path) -> Spawn cmd.arg("server"); cmd.env("XDG_CONFIG_HOME", config_home); cmd.env("XDG_RUNTIME_DIR", runtime_dir); + cmd.env("XDG_STATE_HOME", runtime_dir.join("state")); cmd.env_remove("HERDR_SESSION"); cmd.env_remove("HERDR_SOCKET_PATH"); cmd.env_remove("HERDR_CLIENT_SOCKET_PATH"); @@ -301,6 +302,58 @@ fn wait_for_api(socket_path: &Path, timeout: Duration) { ); } +fn write_plugin_manifest(root: &Path, plugin_id: &str) { + fs::create_dir_all(root).unwrap(); + fs::write( + root.join("herdr-plugin.toml"), + format!( + r#"id = "{plugin_id}" +name = "Live handoff test" +version = "0.1.0" +min_herdr_version = "0.6.10" +platforms = ["linux", "macos", "windows"] +"# + ), + ) + .unwrap(); +} + +fn link_plugin(socket_path: &Path, root: &Path) { + assert_ok(request( + socket_path, + serde_json::json!({ + "id": "test:plugin:link", + "method": "plugin.link", + "params": {"path": root, "enabled": true} + }), + )); +} + +fn listed_plugin_ids(socket_path: &Path) -> Vec<String> { + let response = request( + socket_path, + serde_json::json!({"id":"test:plugin:list","method":"plugin.list","params":{}}), + ); + assert_ok(response.clone()); + response["result"]["plugins"] + .as_array() + .unwrap() + .iter() + .map(|plugin| plugin["plugin_id"].as_str().unwrap().to_string()) + .collect() +} + +fn saved_plugin_ids(registry_path: &Path) -> Vec<String> { + let mut ids = + serde_json::from_str::<Vec<serde_json::Value>>(&fs::read_to_string(registry_path).unwrap()) + .unwrap() + .into_iter() + .map(|plugin| plugin["plugin_id"].as_str().unwrap().to_string()) + .collect::<Vec<_>>(); + ids.sort(); + ids +} + fn wait_for_output(socket_path: &Path, pane_id: &str, needle: &str) { let deadline = Instant::now() + Duration::from_secs(5); let mut last_text = String::new(); @@ -670,6 +723,53 @@ fn live_handoff_preserves_client_socket_env_without_api_socket_env() { } #[test] +fn live_handoff_preserves_installed_plugins() { + let _lock = test_lock(); + let base = unique_test_dir(); + let config_home = base.join("config"); + let runtime_dir = base.join("runtime"); + let api_socket = config_home.join("herdr-dev/herdr.sock"); + let registry_path = config_home.join("herdr-dev/plugins.json"); + let existing_plugin = base.join("plugins/existing"); + let added_plugin = base.join("plugins/added"); + write_plugin_manifest(&existing_plugin, "test.live-handoff-existing"); + write_plugin_manifest(&added_plugin, "test.live-handoff-added"); + + let spawned = spawn_default_session_server(&config_home, &runtime_dir); + wait_for_socket(&api_socket, Duration::from_secs(10)); + register_runtime_dir(&runtime_dir); + + link_plugin(&api_socket, &existing_plugin); + assert_eq!( + listed_plugin_ids(&api_socket), + ["test.live-handoff-existing"] + ); + + assert_ok(request( + &api_socket, + serde_json::json!({"id":"test:handoff","method":"server.live_handoff","params":{}}), + )); + drop(spawned); + wait_for_api(&api_socket, Duration::from_secs(10)); + + assert_eq!( + listed_plugin_ids(&api_socket), + ["test.live-handoff-existing"] + ); + link_plugin(&api_socket, &added_plugin); + assert_eq!( + saved_plugin_ids(®istry_path), + ["test.live-handoff-added", "test.live-handoff-existing"] + ); + + let _ = request( + &api_socket, + serde_json::json!({"id":"test:stop","method":"server.stop","params":{}}), + ); + cleanup_test_base(&base); +} + +#[test] fn live_handoff_preserves_pane_process_io() { let _lock = test_lock(); let base = unique_test_dir(); |
