aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 00:32:15 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 00:32:22 +0300
commita22454f27ce096585e19d1787dba43f56d1505cf (patch)
treebd90ad713146e710b59ff1532b9f867bc42cb07f
parente9cbcf2f6f8654dd41b10826ef6aecd526f6b0b0 (diff)
fix: preserve plugins during live handoff
refs #893
-rw-r--r--docs/next/CHANGELOG.md3
-rw-r--r--src/app/mod.rs1
-rw-r--r--tests/live_handoff.rs100
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(&registry_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();