diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-29 20:39:54 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-29 20:39:54 +0300 |
| commit | 8b74fb8a12716269c76f0b503ee7db5bbec2c921 (patch) | |
| tree | e5230f94cb1e6eb70d1f00c75cd85dae8b5b9ead | |
| parent | 5fffc59cd8aaea1604abb90e749e58fd97acfac0 (diff) | |
feat: emit placement mutation events
| -rw-r--r-- | docs/next/CHANGELOG.md | 3 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/socket-api.mdx | 19 | ||||
| -rw-r--r-- | src/api/schema/events.rs | 29 | ||||
| -rw-r--r-- | src/api/schema/response.rs | 3 | ||||
| -rw-r--r-- | src/api/schema/tests.rs | 75 | ||||
| -rw-r--r-- | src/api/subscriptions.rs | 8 | ||||
| -rw-r--r-- | src/app/actions.rs | 20 | ||||
| -rw-r--r-- | src/app/api/layouts.rs | 6 | ||||
| -rw-r--r-- | src/app/api/plugins/context.rs | 6 | ||||
| -rw-r--r-- | src/app/api/tabs.rs | 76 | ||||
| -rw-r--r-- | src/app/api/workspaces.rs | 90 | ||||
| -rw-r--r-- | src/protocol/wire.rs | 4 | ||||
| -rw-r--r-- | tests/api_ping.rs | 2 | ||||
| -rw-r--r-- | tests/cli_wrapper.rs | 12 | ||||
| -rw-r--r-- | tests/client_mode.rs | 25 | ||||
| -rw-r--r-- | tests/cross_area.rs | 18 | ||||
| -rw-r--r-- | tests/detach_reattach.rs | 58 | ||||
| -rw-r--r-- | tests/multi_client.rs | 4 | ||||
| -rw-r--r-- | tests/server_headless.rs | 22 | ||||
| -rw-r--r-- | tests/support/mod.rs | 1 |
20 files changed, 333 insertions, 148 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index b2b26f37..553f4044 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Changed +- Bumped the client/server protocol version to 15 for socket API placement mutation event and response compatibility. + ### Fixed - Native Windows clients running inside Alacritty now preserve mouse reports and `ctrl+j` input instead of leaking mouse escape sequences into panes. `shift+enter` remains dependent on whether the outer terminal reports it as a distinct modified Enter key. (#792) diff --git a/docs/next/website/src/content/docs/socket-api.mdx b/docs/next/website/src/content/docs/socket-api.mdx index 0db2e3c6..d651cc20 100644 --- a/docs/next/website/src/content/docs/socket-api.mdx +++ b/docs/next/website/src/content/docs/socket-api.mdx @@ -83,11 +83,11 @@ Raw socket method names use dot notation: | Server | `ping`, `server.stop`, `server.reload_config`, `server.agent_manifests`, `server.reload_agent_manifests` | | Notification | `notification.show` | | Client | `client.window_title.set`, `client.window_title.clear` | -| Workspace | `workspace.create`, `workspace.list`, `workspace.get`, `workspace.focus`, `workspace.rename`, `workspace.close` | +| Workspace | `workspace.create`, `workspace.list`, `workspace.get`, `workspace.focus`, `workspace.rename`, `workspace.move`, `workspace.close` | | Worktree | `worktree.list`, `worktree.create`, `worktree.open`, `worktree.remove` | -| Tab | `tab.create`, `tab.list`, `tab.get`, `tab.focus`, `tab.rename`, `tab.close` | +| Tab | `tab.create`, `tab.list`, `tab.get`, `tab.focus`, `tab.rename`, `tab.move`, `tab.close` | | Pane | `pane.split`, `pane.swap`, `pane.move`, `pane.zoom`, `pane.layout`, `pane.process_info`, `pane.neighbor`, `pane.edges`, `pane.focus_direction`, `pane.resize`, `pane.list`, `pane.current`, `pane.get`, `pane.rename`, `pane.send_text`, `pane.send_keys`, `pane.send_input`, `pane.read`, `pane.report_agent`, `pane.report_agent_session`, `pane.report_metadata`, `pane.clear_agent_authority`, `pane.release_agent`, `pane.close`, `pane.wait_for_output` | -| Layout | `layout.export`, `layout.apply` | +| Layout | `layout.export`, `layout.apply`, `layout.set_split_ratio` | | Agent | `agent.list`, `agent.get`, `agent.read`, `agent.explain`, `agent.send`, `agent.rename`, `agent.focus`, `agent.start` | | Events | `events.subscribe`, `events.wait` | | Integrations | `integration.install`, `integration.uninstall` | @@ -177,6 +177,13 @@ not preserve live PTYs, scrollback, or running processes. } ``` +`layout.set_split_ratio` updates an existing split in a tab layout. The response +is `type: "layout_split_ratio_set"` with the updated portable `layout`. + +```json +{"id":"req_ratio","method":"layout.set_split_ratio","params":{"tab_id":"w1:t1","path":[],"ratio":0.6}} +``` + Process-launching methods accept an `env` object. Herdr applies those key/value pairs to the newly launched process only. Herdr also injects `HERDR_SOCKET_PATH`, `HERDR_ENV=1`, `HERDR_WORKSPACE_ID`, `HERDR_TAB_ID`, and `HERDR_PANE_ID` into @@ -587,7 +594,11 @@ Subscribe to events when you need a long-lived stream: The first response acknowledges the subscription. Later lines are pushed events. -Workspace event subscriptions include `workspace.created`, `workspace.updated`, `workspace.renamed`, `workspace.closed`, and `workspace.focused`. Workspace events describe Herdr UI/runtime lifecycle. `workspace.created` includes optional `workspace.worktree` provenance when the workspace belongs to a worktree group. `workspace.closed` includes a final `workspace` snapshot when Herdr can still identify it before removal. +Workspace event subscriptions include `workspace.created`, `workspace.updated`, `workspace.renamed`, `workspace.moved`, `workspace.closed`, and `workspace.focused`. Workspace events describe Herdr UI/runtime lifecycle. `workspace.created` includes optional `workspace.worktree` provenance when the workspace belongs to a worktree group. `workspace.moved` includes the moved `workspace_id`, requested `insert_index`, and updated ordered `workspaces` list. `workspace.closed` includes a final `workspace` snapshot when Herdr can still identify it before removal. +Tab event subscriptions include `tab.created`, `tab.closed`, `tab.focused`, +`tab.renamed`, and `tab.moved`. `tab.moved` includes the moved `tab_id`, +`workspace_id`, requested `insert_index`, and updated ordered `tabs` list for +that workspace. Pane event subscriptions include `pane.created`, `pane.closed`, `pane.focused`, `pane.moved`, `pane.exited`, `pane.agent_detected`, `pane.output_matched`, and `pane.agent_status_changed`. diff --git a/src/api/schema/events.rs b/src/api/schema/events.rs index 6e489180..014288b1 100644 --- a/src/api/schema/events.rs +++ b/src/api/schema/events.rs @@ -22,6 +22,8 @@ pub enum Subscription { WorkspaceUpdated {}, #[serde(rename = "workspace.renamed")] WorkspaceRenamed {}, + #[serde(rename = "workspace.moved")] + WorkspaceMoved {}, #[serde(rename = "workspace.closed")] WorkspaceClosed {}, #[serde(rename = "workspace.focused")] @@ -40,6 +42,8 @@ pub enum Subscription { TabFocused {}, #[serde(rename = "tab.renamed")] TabRenamed {}, + #[serde(rename = "tab.moved")] + TabMoved {}, #[serde(rename = "pane.created")] PaneCreated {}, #[serde(rename = "pane.closed")] @@ -115,6 +119,9 @@ pub enum EventMatch { #[serde(default, skip_serializing_if = "Option::is_none")] label: Option<String>, }, + WorkspaceMoved { + workspace_id: String, + }, WorkspaceFocused { workspace_id: String, }, @@ -132,6 +139,9 @@ pub enum EventMatch { #[serde(default, skip_serializing_if = "Option::is_none")] label: Option<String>, }, + TabMoved { + tab_id: String, + }, TabFocused { tab_id: String, }, @@ -176,6 +186,7 @@ pub enum EventKind { WorkspaceUpdated, WorkspaceClosed, WorkspaceRenamed, + WorkspaceMoved, WorkspaceFocused, WorktreeCreated, WorktreeOpened, @@ -183,6 +194,7 @@ pub enum EventKind { TabCreated, TabClosed, TabRenamed, + TabMoved, TabFocused, PaneCreated, PaneClosed, @@ -201,6 +213,7 @@ impl EventKind { EventKind::WorkspaceUpdated => "workspace.updated", EventKind::WorkspaceClosed => "workspace.closed", EventKind::WorkspaceRenamed => "workspace.renamed", + EventKind::WorkspaceMoved => "workspace.moved", EventKind::WorkspaceFocused => "workspace.focused", EventKind::WorktreeCreated => "worktree.created", EventKind::WorktreeOpened => "worktree.opened", @@ -208,6 +221,7 @@ impl EventKind { EventKind::TabCreated => "tab.created", EventKind::TabClosed => "tab.closed", EventKind::TabRenamed => "tab.renamed", + EventKind::TabMoved => "tab.moved", EventKind::TabFocused => "tab.focused", EventKind::PaneCreated => "pane.created", EventKind::PaneClosed => "pane.closed", @@ -227,6 +241,7 @@ pub const KNOWN_EVENT_KINDS: &[EventKind] = &[ EventKind::WorkspaceUpdated, EventKind::WorkspaceClosed, EventKind::WorkspaceRenamed, + EventKind::WorkspaceMoved, EventKind::WorkspaceFocused, EventKind::WorktreeCreated, EventKind::WorktreeOpened, @@ -234,6 +249,7 @@ pub const KNOWN_EVENT_KINDS: &[EventKind] = &[ EventKind::TabCreated, EventKind::TabClosed, EventKind::TabRenamed, + EventKind::TabMoved, EventKind::TabFocused, EventKind::PaneCreated, EventKind::PaneClosed, @@ -250,6 +266,7 @@ pub const PLUGIN_HOOK_EVENT_KINDS: &[EventKind] = &[ EventKind::WorkspaceUpdated, EventKind::WorkspaceClosed, EventKind::WorkspaceRenamed, + EventKind::WorkspaceMoved, EventKind::WorkspaceFocused, EventKind::WorktreeCreated, EventKind::WorktreeOpened, @@ -257,6 +274,7 @@ pub const PLUGIN_HOOK_EVENT_KINDS: &[EventKind] = &[ EventKind::TabCreated, EventKind::TabClosed, EventKind::TabRenamed, + EventKind::TabMoved, EventKind::TabFocused, EventKind::PaneCreated, EventKind::PaneClosed, @@ -383,6 +401,11 @@ pub enum EventData { workspace_id: String, label: String, }, + WorkspaceMoved { + workspace_id: String, + insert_index: usize, + workspaces: Vec<WorkspaceInfo>, + }, WorkspaceFocused { workspace_id: String, }, @@ -414,6 +437,12 @@ pub enum EventData { workspace_id: String, label: String, }, + TabMoved { + tab_id: String, + workspace_id: String, + insert_index: usize, + tabs: Vec<TabInfo>, + }, TabFocused { tab_id: String, workspace_id: String, diff --git a/src/api/schema/response.rs b/src/api/schema/response.rs index 51ea0d36..57ab215b 100644 --- a/src/api/schema/response.rs +++ b/src/api/schema/response.rs @@ -130,6 +130,9 @@ pub enum ResponseResult { LayoutApply { layout: LayoutDescription, }, + LayoutSplitRatioSet { + layout: LayoutDescription, + }, PaneNeighbor { neighbor: PaneNeighborResult, }, diff --git a/src/api/schema/tests.rs b/src/api/schema/tests.rs index d9ebe30b..dd2004cf 100644 --- a/src/api/schema/tests.rs +++ b/src/api/schema/tests.rs @@ -250,18 +250,39 @@ fn pane_process_info_request_round_trips() { #[test] fn event_envelope_round_trips() { - let event = EventEnvelope { - event: EventKind::PaneOutputChanged, - data: EventData::PaneOutputChanged { - pane_id: "p_1".into(), - workspace_id: "w_1".into(), - revision: 42, + let events = [ + EventEnvelope { + event: EventKind::PaneOutputChanged, + data: EventData::PaneOutputChanged { + pane_id: "p_1".into(), + workspace_id: "w_1".into(), + revision: 42, + }, }, - }; + EventEnvelope { + event: EventKind::WorkspaceMoved, + data: EventData::WorkspaceMoved { + workspace_id: "w_1".into(), + insert_index: 2, + workspaces: vec![], + }, + }, + EventEnvelope { + event: EventKind::TabMoved, + data: EventData::TabMoved { + tab_id: "w_1:1".into(), + workspace_id: "w_1".into(), + insert_index: 1, + tabs: vec![], + }, + }, + ]; - let json = serde_json::to_string(&event).unwrap(); - let restored: EventEnvelope = serde_json::from_str(&json).unwrap(); - assert_eq!(restored, event); + for event in events { + let json = serde_json::to_string(&event).unwrap(); + let restored: EventEnvelope = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, event); + } } #[test] @@ -700,6 +721,28 @@ fn layout_export_apply_round_trip() { let json = serde_json::to_string(&response).unwrap(); let restored: SuccessResponse = serde_json::from_str(&json).unwrap(); assert_eq!(restored, response); + + let response = SuccessResponse { + id: "layout_ratio".into(), + result: ResponseResult::LayoutSplitRatioSet { + layout: LayoutDescription { + workspace_id: "w1".into(), + tab_id: "w1:1".into(), + zoomed: false, + focused_pane_id: "w1-1".into(), + root: LayoutNode::Pane { + pane: LayoutPane { + pane_id: Some("w1-1".into()), + ..Default::default() + }, + }, + }, + }, + }; + let json = serde_json::to_string(&response).unwrap(); + assert!(json.contains("\"type\":\"layout_split_ratio_set\"")); + let restored: SuccessResponse = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, response); } #[test] @@ -752,6 +795,18 @@ fn authority_mutation_requests_round_trip() { assert_eq!(json["method"], "layout.set_split_ratio"); let restored: Request = serde_json::from_value(json).unwrap(); assert_eq!(restored, split_ratio); + + let subscription = Request { + id: "sub_moves".into(), + method: Method::EventsSubscribe(EventsSubscribeParams { + subscriptions: vec![Subscription::WorkspaceMoved {}, Subscription::TabMoved {}], + }), + }; + let json = serde_json::to_string(&subscription).unwrap(); + assert!(json.contains("\"type\":\"workspace.moved\"")); + assert!(json.contains("\"type\":\"tab.moved\"")); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, subscription); } #[test] diff --git a/src/api/subscriptions.rs b/src/api/subscriptions.rs index d71ea24d..f3b82090 100644 --- a/src/api/subscriptions.rs +++ b/src/api/subscriptions.rs @@ -120,6 +120,10 @@ impl ActiveSubscription { event_kind: crate::api::schema::EventKind::WorkspaceRenamed, last_sequence: 0, })), + Subscription::WorkspaceMoved {} => Ok(Self::Event(ActiveEventSubscription { + event_kind: crate::api::schema::EventKind::WorkspaceMoved, + last_sequence: 0, + })), Subscription::WorkspaceClosed {} => Ok(Self::Event(ActiveEventSubscription { event_kind: crate::api::schema::EventKind::WorkspaceClosed, last_sequence: 0, @@ -156,6 +160,10 @@ impl ActiveSubscription { event_kind: crate::api::schema::EventKind::TabRenamed, last_sequence: 0, })), + Subscription::TabMoved {} => Ok(Self::Event(ActiveEventSubscription { + event_kind: crate::api::schema::EventKind::TabMoved, + last_sequence: 0, + })), Subscription::PaneCreated {} => Ok(Self::Event(ActiveEventSubscription { event_kind: crate::api::schema::EventKind::PaneCreated, last_sequence: 0, diff --git a/src/app/actions.rs b/src/app/actions.rs index d791da03..eccc8d4d 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -1193,9 +1193,18 @@ impl AppState { self.switch_workspace(prev); } - pub fn move_workspace(&mut self, source_idx: usize, insert_idx: usize) { + pub fn move_workspace(&mut self, source_idx: usize, insert_idx: usize) -> bool { if source_idx >= self.workspaces.len() || insert_idx > self.workspaces.len() { - return; + return false; + } + + let target_idx = if source_idx < insert_idx { + insert_idx - 1 + } else { + insert_idx + }; + if source_idx == target_idx { + return false; } self.mark_session_dirty(); @@ -1207,12 +1216,6 @@ impl AppState { .map(|workspace| workspace.id.clone()); let workspace = self.workspaces.remove(source_idx); - let target_idx = if source_idx < insert_idx { - insert_idx.saturating_sub(1) - } else { - insert_idx - } - .min(self.workspaces.len()); self.workspaces.insert(target_idx, workspace); self.active = active_id.and_then(|id| self.workspaces.iter().position(|ws| ws.id == id)); @@ -1220,6 +1223,7 @@ impl AppState { .and_then(|id| self.workspaces.iter().position(|ws| ws.id == id)) .unwrap_or(0); self.ensure_workspace_visible(self.selected); + true } pub fn scroll_tabs_left(&mut self) { diff --git a/src/app/api/layouts.rs b/src/app/api/layouts.rs index 709d9f39..c7c02f5f 100644 --- a/src/app/api/layouts.rs +++ b/src/app/api/layouts.rs @@ -243,7 +243,7 @@ impl App { let Some(layout) = self.layout_description(ws_idx, tab_idx) else { return encode_error(id, "layout_not_found", "layout unavailable"); }; - encode_success(id, ResponseResult::LayoutApply { layout }) + encode_success(id, ResponseResult::LayoutSplitRatioSet { layout }) } fn resolve_layout_export_target(&self, params: &LayoutExportParams) -> Option<(usize, usize)> { @@ -684,8 +684,8 @@ mod tests { ); let success: SuccessResponse = serde_json::from_str(&response).unwrap(); - let ResponseResult::LayoutApply { layout } = success.result else { - panic!("expected layout apply response"); + let ResponseResult::LayoutSplitRatioSet { layout } = success.result else { + panic!("expected layout split ratio set response"); }; let LayoutNode::Split { ratio, .. } = layout.root else { panic!("expected split layout root"); diff --git a/src/app/api/plugins/context.rs b/src/app/api/plugins/context.rs index 92d0d4c6..e81a74d3 100644 --- a/src/app/api/plugins/context.rs +++ b/src/app/api/plugins/context.rs @@ -63,6 +63,7 @@ impl App { }) }), EventData::WorkspaceRenamed { workspace_id, .. } + | EventData::WorkspaceMoved { workspace_id, .. } | EventData::WorkspaceFocused { workspace_id } => self .plugin_context_for_workspace_id(workspace_id, correlation_id) .unwrap_or_else(|| { @@ -103,6 +104,11 @@ impl App { workspace_id, .. } + | EventData::TabMoved { + tab_id, + workspace_id, + .. + } | EventData::TabFocused { tab_id, workspace_id, diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs index 1ffcafdd..abd05b29 100644 --- a/src/app/api/tabs.rs +++ b/src/app/api/tabs.rs @@ -14,12 +14,10 @@ impl App { let Some(ws_idx) = self.parse_workspace_id(&workspace_id) else { return workspace_not_found(id, &workspace_id); }; - let Some(ws) = self.state.workspaces.get(ws_idx) else { + let Some(_) = self.state.workspaces.get(ws_idx) else { return workspace_not_found(id, &workspace_id); }; - (0..ws.tabs.len()) - .filter_map(|tab_idx| self.tab_info(ws_idx, tab_idx)) - .collect() + self.tab_list_info(ws_idx) } else { let mut tabs = Vec::new(); for (ws_idx, ws) in self.state.workspaces.iter().enumerate() { @@ -190,34 +188,35 @@ impl App { ); } + let tab_id = self + .public_tab_id(ws_idx, tab_idx) + .unwrap_or_else(|| crate::workspace::public_tab_id_for_number(&ws.id, tab_idx + 1)); + let workspace_id = self.public_workspace_id(ws_idx); + let insert_index = params.insert_index; let moved = self .state .workspaces .get_mut(ws_idx) - .is_some_and(|ws| ws.move_tab(tab_idx, params.insert_index)); + .is_some_and(|ws| ws.move_tab(tab_idx, insert_index)); + let tabs = self.tab_list_info(ws_idx); if moved { self.schedule_session_save(); if self.state.active == Some(ws_idx) { self.state.tab_scroll_follow_active = true; self.state.refresh_tab_bar_view(); } + self.emit_event(EventEnvelope { + event: EventKind::TabMoved, + data: EventData::TabMoved { + tab_id, + workspace_id, + insert_index, + tabs: tabs.clone(), + }, + }); } - encode_success( - id, - ResponseResult::TabList { - tabs: self - .state - .workspaces - .get(ws_idx) - .map(|ws| { - (0..ws.tabs.len()) - .filter_map(|idx| self.tab_info(ws_idx, idx)) - .collect() - }) - .unwrap_or_default(), - }, - ) + encode_success(id, ResponseResult::TabList { tabs }) } pub(super) fn handle_tab_close(&mut self, id: String, target: TabTarget) -> String { @@ -269,6 +268,18 @@ impl App { encode_success(id, ResponseResult::Ok {}) } + + fn tab_list_info(&self, ws_idx: usize) -> Vec<crate::api::schema::TabInfo> { + self.state + .workspaces + .get(ws_idx) + .map(|ws| { + (0..ws.tabs.len()) + .filter_map(|idx| self.tab_info(ws_idx, idx)) + .collect() + }) + .unwrap_or_default() + } } fn workspace_not_found(id: String, workspace_id: &str) -> String { @@ -290,14 +301,9 @@ mod tests { #[test] fn api_tab_move_reorders_tabs_in_target_workspace() { + let event_hub = crate::api::EventHub::default(); let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); - let mut app = App::new( - &Config::default(), - true, - None, - api_rx, - crate::api::EventHub::default(), - ); + let mut app = App::new(&Config::default(), true, None, api_rx, event_hub.clone()); let mut workspace = Workspace::test_new("tabs"); workspace.test_add_tab(Some("two")); workspace.test_add_tab(Some("three")); @@ -310,7 +316,7 @@ mod tests { let response = app.handle_tab_move( "req".into(), TabMoveParams { - tab_id: moved_id, + tab_id: moved_id.clone(), insert_index: 3, }, ); @@ -321,5 +327,19 @@ mod tests { }; assert_eq!(app.state.workspaces[0].tabs[2].root_pane, moved_root); assert_eq!(tabs[2].tab_id, app.public_tab_id(0, 2).unwrap()); + let events = event_hub.events_after(0); + assert!(events.iter().any(|(_, event)| { + matches!( + &event.data, + EventData::TabMoved { + tab_id, + workspace_id, + insert_index: 3, + tabs, + } if tab_id == &moved_id + && workspace_id == &app.public_workspace_id(0) + && tabs[2].tab_id == moved_id + ) + })); } } diff --git a/src/app/api/workspaces.rs b/src/app/api/workspaces.rs index 56427463..f4189a04 100644 --- a/src/app/api/workspaces.rs +++ b/src/app/api/workspaces.rs @@ -13,13 +13,7 @@ impl App { encode_success( id, ResponseResult::WorkspaceList { - workspaces: self - .state - .workspaces - .iter() - .enumerate() - .map(|(idx, _)| self.workspace_info(idx)) - .collect(), + workspaces: self.workspace_list_info(), }, ) } @@ -140,20 +134,22 @@ impl App { ); } - self.state.move_workspace(index, params.insert_index); + let workspace_id = self.public_workspace_id(index); + let insert_index = params.insert_index; + let moved = self.state.move_workspace(index, insert_index); + let workspaces = self.workspace_list_info(); + if moved { + self.emit_event(EventEnvelope { + event: EventKind::WorkspaceMoved, + data: EventData::WorkspaceMoved { + workspace_id, + insert_index, + workspaces: workspaces.clone(), + }, + }); + } - encode_success( - id, - ResponseResult::WorkspaceList { - workspaces: self - .state - .workspaces - .iter() - .enumerate() - .map(|(idx, _)| self.workspace_info(idx)) - .collect(), - }, - ) + encode_success(id, ResponseResult::WorkspaceList { workspaces }) } pub(super) fn handle_workspace_close(&mut self, id: String, target: WorkspaceTarget) -> String { @@ -192,6 +188,15 @@ impl App { encode_success(id, ResponseResult::Ok {}) } + + fn workspace_list_info(&self) -> Vec<crate::api::schema::WorkspaceInfo> { + self.state + .workspaces + .iter() + .enumerate() + .map(|(idx, _)| self.workspace_info(idx)) + .collect() + } } fn workspace_not_found(id: String, workspace_id: &str) -> String { @@ -279,14 +284,9 @@ mod tests { #[test] fn api_workspace_move_reorders_workspaces() { + let event_hub = crate::api::EventHub::default(); let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); - let mut app = App::new( - &Config::default(), - true, - None, - api_rx, - crate::api::EventHub::default(), - ); + let mut app = App::new(&Config::default(), true, None, api_rx, event_hub.clone()); app.state.workspaces = vec![ Workspace::test_new("one"), Workspace::test_new("two"), @@ -310,5 +310,41 @@ mod tests { }; assert_eq!(workspaces[2].workspace_id, moved_id); assert_eq!(app.state.workspaces[2].display_name(), "one"); + let events = event_hub.events_after(0); + assert!(events.iter().any(|(_, event)| { + matches!( + &event.data, + EventData::WorkspaceMoved { + workspace_id, + insert_index: 3, + workspaces, + } if workspace_id == &moved_id + && workspaces[2].workspace_id == moved_id + ) + })); + } + + #[test] + fn api_workspace_move_noop_does_not_emit_event() { + let event_hub = crate::api::EventHub::default(); + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new(&Config::default(), true, None, api_rx, event_hub.clone()); + app.state.workspaces = vec![Workspace::test_new("one"), Workspace::test_new("two")]; + let moved_id = app.public_workspace_id(0); + + let response = app.handle_workspace_move( + "req".into(), + WorkspaceMoveParams { + workspace_id: moved_id.clone(), + insert_index: 1, + }, + ); + + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + let ResponseResult::WorkspaceList { workspaces } = success.result else { + panic!("expected workspace list"); + }; + assert_eq!(workspaces[0].workspace_id, moved_id); + assert!(event_hub.events_after(0).is_empty()); } } diff --git a/src/protocol/wire.rs b/src/protocol/wire.rs index 1bbfd9a8..ac21df19 100644 --- a/src/protocol/wire.rs +++ b/src/protocol/wire.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; // --------------------------------------------------------------------------- /// Current protocol version. Bumped when wire format changes incompatibly. -pub const PROTOCOL_VERSION: u32 = 14; +pub const PROTOCOL_VERSION: u32 = 15; /// Maximum allowed frame payload size (2 MB). Frames larger than this are /// rejected to prevent denial-of-service via oversized length prefixes. @@ -935,7 +935,7 @@ mod tests { } #[test] - fn client_message_wire_tags_preserve_protocol_14_order() { + fn client_message_wire_tags_preserve_protocol_15_order() { fn tag(msg: &ClientMessage) -> u8 { *bincode::serde::encode_to_vec(msg, bincode::config::standard()) .unwrap() diff --git a/tests/api_ping.rs b/tests/api_ping.rs index 1f9fafe6..1d625637 100644 --- a/tests/api_ping.rs +++ b/tests/api_ping.rs @@ -304,7 +304,7 @@ fn ping_over_socket_returns_version() { assert_eq!(value["result"]["version"], env!("CARGO_PKG_VERSION")); // Intentionally hardcoded so wire protocol bumps require updating this test. // Changing this value means old clients/servers are no longer compatible. - assert_eq!(value["result"]["protocol"], 14); + assert_eq!(value["result"]["protocol"], 15); cleanup_spawned_herdr(child, base); } diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs index 55555be5..542d9768 100644 --- a/tests/cli_wrapper.rs +++ b/tests/cli_wrapper.rs @@ -1441,7 +1441,7 @@ fn status_commands_report_client_and_server_versions() { "stdout: {full_stdout}" ); assert!( - full_stdout.contains(" protocol: 14"), + full_stdout.contains(" protocol: 15"), "stdout: {full_stdout}" ); assert!(full_stdout.contains("server:\n"), "stdout: {full_stdout}"); @@ -1474,7 +1474,7 @@ fn status_commands_report_client_and_server_versions() { "stdout: {server_stdout}" ); assert!( - server_stdout.contains("protocol: 14"), + server_stdout.contains("protocol: 15"), "stdout: {server_stdout}" ); @@ -1486,7 +1486,7 @@ fn status_commands_report_client_and_server_versions() { "stdout: {client_stdout}" ); assert!( - client_stdout.contains("protocol: 14"), + client_stdout.contains("protocol: 15"), "stdout: {client_stdout}" ); assert!( @@ -1496,7 +1496,7 @@ fn status_commands_report_client_and_server_versions() { let full_json = run_cli_json(&socket_path, &["status", "--json"]); assert_eq!(full_json["client"]["version"], env!("CARGO_PKG_VERSION")); - assert_eq!(full_json["client"]["protocol"], 14); + assert_eq!(full_json["client"]["protocol"], 15); assert_eq!(full_json["server"]["status"], "running"); assert_eq!(full_json["server"]["running"], true); assert_eq!(full_json["server"]["compatible"], true); @@ -1510,12 +1510,12 @@ fn status_commands_report_client_and_server_versions() { let server_json = run_cli_json(&socket_path, &["status", "server", "--json"]); assert_eq!(server_json["status"], "running"); assert_eq!(server_json["version"], env!("CARGO_PKG_VERSION")); - assert_eq!(server_json["protocol"], 14); + assert_eq!(server_json["protocol"], 15); assert_eq!(server_json["compatible"], true); let client_json = run_cli_json(&socket_path, &["status", "client", "--json"]); assert_eq!(client_json["version"], env!("CARGO_PKG_VERSION")); - assert_eq!(client_json["protocol"], 14); + assert_eq!(client_json["protocol"], 15); assert!(client_json["binary"] .as_str() .is_some_and(|path| !path.is_empty())); diff --git a/tests/client_mode.rs b/tests/client_mode.rs index 4f628faa..c1b62394 100644 --- a/tests/client_mode.rs +++ b/tests/client_mode.rs @@ -16,7 +16,7 @@ use serde_json::Value; use support::{ cleanup_test_base, client_handshake, encode_varint_u32, frame_message, read_server_message, register_runtime_dir, register_spawned_herdr_pid, unregister_spawned_herdr_pid, - wait_for_message_variant, wait_for_socket, wait_until, + wait_for_message_variant, wait_for_socket, wait_until, CURRENT_PROTOCOL, }; fn unique_test_dir() -> PathBuf { @@ -311,8 +311,11 @@ fn client_connects_and_receives_frame() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14, "server should report protocol version 14"); + client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed"); + assert_eq!( + version, CURRENT_PROTOCOL, + "server should report current protocol version" + ); assert!( error.is_none(), "handshake should not have error: {:?}", @@ -379,8 +382,8 @@ fn client_sees_headless_startup_config_diagnostic() { let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14); + client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); stream @@ -595,8 +598,8 @@ fn client_receives_frame_after_pane_output() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14); + client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); read_next_frame_payload(&mut stream, Duration::from_secs(10)) @@ -710,8 +713,8 @@ fn graceful_shutdown_sends_server_shutdown_to_client() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14); + client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); // Drain initial frame(s). @@ -809,8 +812,8 @@ fn client_receives_notify_on_agent_state_change() { // Connect as a client and perform handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect"); let (version, error) = - client_handshake(&mut stream, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14); + client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); // Drain initial frame(s). diff --git a/tests/cross_area.rs b/tests/cross_area.rs index d596a8fc..9fe71790 100644 --- a/tests/cross_area.rs +++ b/tests/cross_area.rs @@ -15,7 +15,7 @@ use serde::Deserialize; use serde_json::{json, Value}; use support::{ cleanup_test_base, register_runtime_dir, register_spawned_herdr_pid, - unregister_spawned_herdr_pid, + unregister_spawned_herdr_pid, CURRENT_PROTOCOL, }; fn unique_test_dir() -> PathBuf { @@ -687,7 +687,7 @@ fn cross_area_detach_and_reattach_preserves_state() { // Local attach (client A). let mut client_a = UnixStream::connect(&client_socket).expect("client A should connect"); - client_handshake(&mut client_a, 14, 100, 30); + client_handshake(&mut client_a, CURRENT_PROTOCOL, 100, 30); assert!(wait_for_frame(&mut client_a, Duration::from_secs(2))); // Use herdr: create a workspace and write output into its pane. @@ -724,7 +724,7 @@ fn cross_area_detach_and_reattach_preserves_state() { // Reattach from another terminal/session (client B). let mut client_b = UnixStream::connect(&client_socket).expect("client B should connect"); - client_handshake(&mut client_b, 14, 80, 24); + client_handshake(&mut client_b, CURRENT_PROTOCOL, 80, 24); assert!( wait_for_frame(&mut client_b, Duration::from_secs(5)), "reattached client should receive frame" @@ -780,7 +780,7 @@ fn cross_area_agent_process_survives_detach_and_reattach() { wait_for_socket(&client_socket, Duration::from_secs(10)); let mut client_a = UnixStream::connect(&client_socket).expect("client A should connect"); - client_handshake(&mut client_a, 14, 100, 30); + client_handshake(&mut client_a, CURRENT_PROTOCOL, 100, 30); assert!(wait_for_frame(&mut client_a, Duration::from_secs(2))); let created = workspace_create(&api_socket, "agent-persist"); @@ -833,7 +833,7 @@ fn cross_area_agent_process_survives_detach_and_reattach() { // Reattach and ensure client-side state reflects the persisted working status. let mut client_b = UnixStream::connect(&client_socket).expect("client B should connect"); - client_handshake(&mut client_b, 14, 80, 24); + client_handshake(&mut client_b, CURRENT_PROTOCOL, 80, 24); let saw_working_on_client = wait_for_frame_matching(&mut client_b, Duration::from_secs(5), |frame| { frame_contains_text(frame, "working") @@ -878,7 +878,7 @@ fn cross_area_client_and_api_workspace_views_are_consistent() { wait_for_socket(&client_socket, Duration::from_secs(10)); let mut client = UnixStream::connect(&client_socket).expect("client should connect"); - client_handshake(&mut client, 14, 100, 30); + client_handshake(&mut client, CURRENT_PROTOCOL, 100, 30); assert!(wait_for_frame(&mut client, Duration::from_secs(2))); drain_server_messages(&mut client, Duration::from_millis(300)); @@ -941,9 +941,9 @@ fn cross_area_two_clients_shared_view_and_single_detach_stability() { wait_for_socket(&client_socket, Duration::from_secs(10)); let mut client_a = UnixStream::connect(&client_socket).expect("client A should connect"); - client_handshake(&mut client_a, 14, 110, 30); + client_handshake(&mut client_a, CURRENT_PROTOCOL, 110, 30); let mut client_b = UnixStream::connect(&client_socket).expect("client B should connect"); - client_handshake(&mut client_b, 14, 100, 30); + client_handshake(&mut client_b, CURRENT_PROTOCOL, 100, 30); assert!(wait_for_frame(&mut client_a, Duration::from_secs(2))); assert!(wait_for_frame(&mut client_b, Duration::from_secs(2))); @@ -1112,7 +1112,7 @@ fn cross_area_server_kill_then_restart_and_reconnect() { let mut reconnect_client = UnixStream::connect(&client_socket).expect("new client should connect after restart"); - client_handshake(&mut reconnect_client, 14, 80, 24); + client_handshake(&mut reconnect_client, CURRENT_PROTOCOL, 80, 24); assert!( wait_for_frame(&mut reconnect_client, Duration::from_secs(5)), "new client should receive frame after restart" diff --git a/tests/detach_reattach.rs b/tests/detach_reattach.rs index cfcb5b89..3abe1bec 100644 --- a/tests/detach_reattach.rs +++ b/tests/detach_reattach.rs @@ -16,7 +16,7 @@ use serde_json::Value; use support::{ cleanup_test_base, client_handshake, drain_messages, read_server_message, register_runtime_dir, register_spawned_herdr_pid, send_detach, send_input, unregister_spawned_herdr_pid, - wait_for_disconnect, wait_for_message_variant, wait_for_socket, wait_until, + wait_for_disconnect, wait_for_message_variant, wait_for_socket, wait_until, CURRENT_PROTOCOL, }; fn unique_test_dir() -> PathBuf { @@ -276,8 +276,8 @@ fn navigate_q_detaches_client_and_server_persists() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14); + client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); // Drain initial frames. @@ -338,8 +338,8 @@ fn explicit_detach_message_causes_clean_disconnect() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect"); let (version, error) = - client_handshake(&mut stream, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14); + client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); // Drain initial frames. @@ -396,9 +396,9 @@ fn reattach_after_detach_shows_current_state() { // --- Client A --- let mut stream_a = UnixStream::connect(&client_socket).expect("client A should connect"); - let (version, error) = - client_handshake(&mut stream_a, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14); + let (version, error) = client_handshake(&mut stream_a, CURRENT_PROTOCOL, 80, 24) + .expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); // Drain initial frames. @@ -435,9 +435,9 @@ fn reattach_after_detach_shows_current_state() { // --- Client B (reattach) --- let mut stream_b = UnixStream::connect(&client_socket).expect("client B should connect"); - let (version, error) = - client_handshake(&mut stream_b, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14); + let (version, error) = client_handshake(&mut stream_b, CURRENT_PROTOCOL, 80, 24) + .expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!( error.is_none(), "reattach handshake should succeed: {:?}", @@ -516,8 +516,8 @@ fn processes_survive_during_and_after_detach() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect"); let (version, error) = - client_handshake(&mut stream, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14); + client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); // Drain initial frames. @@ -554,9 +554,9 @@ fn processes_survive_during_and_after_detach() { // Reattach — verify we can connect and receive a frame. let mut stream_b = UnixStream::connect(&client_socket).expect("should reattach"); - let (version, error) = - client_handshake(&mut stream_b, 14, 80, 24).expect("reattach handshake should succeed"); - assert_eq!(version, 14); + let (version, error) = client_handshake(&mut stream_b, CURRENT_PROTOCOL, 80, 24) + .expect("reattach handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); // Verify the reattached client receives a frame. @@ -604,8 +604,8 @@ fn server_persists_after_client_connection_drop() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect"); let (version, error) = - client_handshake(&mut stream, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14); + client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); // Drain initial frames. @@ -630,9 +630,9 @@ fn server_persists_after_client_connection_drop() { // Reattach — verify we can connect and handshake again. let mut stream_b = UnixStream::connect(&client_socket).expect("should reattach"); - let (version, error) = - client_handshake(&mut stream_b, 14, 80, 24).expect("reattach handshake should succeed"); - assert_eq!(version, 14); + let (version, error) = client_handshake(&mut stream_b, CURRENT_PROTOCOL, 80, 24) + .expect("reattach handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "reattach should succeed: {:?}", error); cleanup_spawned_herdr(spawned, base); @@ -653,8 +653,8 @@ fn detached_output_preserves_last_attached_pty_size() { let mut stream = UnixStream::connect(&client_socket).expect("client should connect"); let (version, error) = - client_handshake(&mut stream, 14, 120, 40).expect("handshake should succeed"); - assert_eq!(version, 14); + client_handshake(&mut stream, CURRENT_PROTOCOL, 120, 40).expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); drain_messages(&mut stream); @@ -721,9 +721,9 @@ fn output_accumulated_while_detached_visible_on_reattach() { // Connect and handshake client A. let mut stream_a = UnixStream::connect(&client_socket).expect("client A should connect"); - let (version, error) = - client_handshake(&mut stream_a, 14, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14); + let (version, error) = client_handshake(&mut stream_a, CURRENT_PROTOCOL, 80, 24) + .expect("handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); // Detach client A immediately. @@ -779,9 +779,9 @@ fn output_accumulated_while_detached_visible_on_reattach() { // --- Client B (reattach) --- let mut stream_b = UnixStream::connect(&client_socket).expect("client B should connect"); - let (version, error) = - client_handshake(&mut stream_b, 14, 80, 24).expect("reattach handshake should succeed"); - assert_eq!(version, 14); + let (version, error) = client_handshake(&mut stream_b, CURRENT_PROTOCOL, 80, 24) + .expect("reattach handshake should succeed"); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none(), "{:?}", error); // Client B should receive a frame with the current state. diff --git a/tests/multi_client.rs b/tests/multi_client.rs index 421a6756..d891a9f0 100644 --- a/tests/multi_client.rs +++ b/tests/multi_client.rs @@ -16,7 +16,7 @@ use serde::Deserialize; use serde_json::Value; use support::{ cleanup_test_base, register_runtime_dir, register_spawned_herdr_pid, - unregister_spawned_herdr_pid, + unregister_spawned_herdr_pid, CURRENT_PROTOCOL, }; fn unique_test_dir() -> PathBuf { @@ -566,7 +566,7 @@ fn client_handshake( fn connect_raw_client(client_socket: &Path, cols: u16, rows: u16) -> UnixStream { let mut stream = UnixStream::connect(client_socket).expect("should connect to client socket"); - client_handshake(&mut stream, 14, cols, rows).expect("handshake should succeed"); + client_handshake(&mut stream, CURRENT_PROTOCOL, cols, rows).expect("handshake should succeed"); stream } diff --git a/tests/server_headless.rs b/tests/server_headless.rs index 6e5f03cd..5abb5ace 100644 --- a/tests/server_headless.rs +++ b/tests/server_headless.rs @@ -14,7 +14,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize}; use support::{ cleanup_test_base, register_runtime_dir, register_spawned_herdr_pid, - unregister_spawned_herdr_pid, + unregister_spawned_herdr_pid, CURRENT_PROTOCOL, }; fn unique_test_dir() -> PathBuf { @@ -595,9 +595,12 @@ fn client_handshake_succeeds() { // Send Hello with the current protocol version, 80 cols, 24 rows. let (version, error) = - client_handshake(&mut stream, 14, 80, 24).expect("handshake should succeed"); + client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 14, "server should report protocol version 14"); + assert_eq!( + version, CURRENT_PROTOCOL, + "server should report current protocol version" + ); assert!( error.is_none(), "handshake should not have an error: {:?}", @@ -626,7 +629,10 @@ fn client_handshake_rejects_incompatible_version() { let (version, error) = client_handshake(&mut stream, 0, 80, 24) .expect("should read Welcome response even on rejection"); - assert_eq!(version, 14, "server should report its version 14"); + assert_eq!( + version, CURRENT_PROTOCOL, + "server should report its current protocol version" + ); assert!( error.is_some(), "version 0 should be rejected with an error" @@ -651,10 +657,10 @@ fn client_handshake_clamps_small_terminal_size() { // Send Hello with 0x0 terminal size — should be clamped. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); - let (version, error) = client_handshake(&mut stream, 14, 0, 0) + let (version, error) = client_handshake(&mut stream, CURRENT_PROTOCOL, 0, 0) .expect("handshake with 0x0 should succeed (server clamps)"); - assert_eq!(version, 14); + assert_eq!(version, CURRENT_PROTOCOL); assert!( error.is_none(), "0x0 size should be accepted (clamped): {:?}", @@ -714,9 +720,9 @@ fn no_hello_client_closed_within_five_seconds() { // Verify the server is still healthy — a proper client can still connect. let mut good_stream = UnixStream::connect(&client_socket).expect("should connect after no-hello client"); - let (version, error) = client_handshake(&mut good_stream, 14, 80, 24) + let (version, error) = client_handshake(&mut good_stream, CURRENT_PROTOCOL, 80, 24) .expect("proper handshake should still work after no-hello client"); - assert_eq!(version, 14); + assert_eq!(version, CURRENT_PROTOCOL); assert!(error.is_none()); // API should still work. diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 5359997f..5844bfa7 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -15,6 +15,7 @@ static INIT: Once = Once::new(); static CLEANUP_GUARD: OnceLock<CleanupGuard> = OnceLock::new(); const WATCHDOG_SCAN_INTERVAL: Duration = Duration::from_secs(1); const RUNTIME_OWNER_MARKER: &str = ".herdr-test-owner-pid"; +pub const CURRENT_PROTOCOL: u32 = 15; pub fn register_spawned_herdr_pid(pid: Option<u32>) { let Some(pid) = pid else { |
