diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-14 02:43:45 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-14 03:47:07 +0300 |
| commit | fbd20ad6e488fb3b9efc1612fdffee6e672976c1 (patch) | |
| tree | d21daf93241786f8aa6fde23b41cdb9e15182b79 /src/api/schema/tests.rs | |
| parent | d35c642ef36ee5ac2afe7c1b18ad5a717bc807d8 (diff) | |
feat: add plugin v1 system
Add manifest-driven plugin actions, event hooks, pane entrypoints, link handlers, and host APIs.
Support plugin link, GitHub install, build commands, installed plugin registry, pane movement, runtime context, and plugin docs.
Diffstat (limited to 'src/api/schema/tests.rs')
| -rw-r--r-- | src/api/schema/tests.rs | 975 |
1 files changed, 975 insertions, 0 deletions
diff --git a/src/api/schema/tests.rs b/src/api/schema/tests.rs new file mode 100644 index 00000000..c1713972 --- /dev/null +++ b/src/api/schema/tests.rs @@ -0,0 +1,975 @@ +use std::collections::HashMap; + +use super::*; + +#[test] +fn request_round_trips_for_pane_read() { + let request = Request { + id: "req_1".into(), + method: Method::PaneRead(PaneReadParams { + pane_id: "p_1".into(), + source: ReadSource::Recent, + lines: Some(80), + format: ReadFormat::Text, + strip_ansi: true, + }), + }; + + let json = serde_json::to_string(&request).unwrap(); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn request_round_trips_for_pane_report_agent() { + let request = Request { + id: "req_hook".into(), + method: Method::PaneReportAgent(PaneReportAgentParams { + pane_id: "1-1".into(), + source: "herdr:pi".into(), + agent: "pi".into(), + state: PaneAgentState::Working, + message: Some("thinking".into()), + custom_status: Some("indexing".into()), + seq: Some(42), + agent_session_id: Some("pi-session".into()), + agent_session_path: Some("/tmp/pi-session.jsonl".into()), + }), + }; + + let json = serde_json::to_string(&request).unwrap(); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn request_round_trips_for_pane_report_agent_session() { + let request = Request { + id: "req_session".into(), + method: Method::PaneReportAgentSession(PaneReportAgentSessionParams { + pane_id: "1-1".into(), + source: "herdr:claude".into(), + agent: "claude".into(), + seq: Some(42), + agent_session_id: Some("claude-session".into()), + agent_session_path: None, + }), + }; + + let json = serde_json::to_string(&request).unwrap(); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn request_round_trips_for_pane_report_metadata() { + let request = Request { + id: "req_metadata".into(), + method: Method::PaneReportMetadata(PaneReportMetadataParams { + pane_id: "1-1".into(), + source: "user:claude-title".into(), + agent: Some("claude".into()), + applies_to_source: Some("herdr:claude".into()), + title: Some("Refactor auth".into()), + display_agent: Some("Claude auth".into()), + custom_status: Some("refactor auth".into()), + state_labels: HashMap::from([("working".into(), "deep in the mines".into())]), + clear_title: false, + clear_display_agent: false, + clear_custom_status: false, + clear_state_labels: false, + seq: Some(42), + ttl_ms: Some(3_600_000), + }), + }; + + let json = serde_json::to_string(&request).unwrap(); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn request_round_trips_for_pane_clear_agent_authority() { + let request = Request { + id: "req_clear".into(), + method: Method::PaneClearAgentAuthority(PaneClearAgentAuthorityParams { + pane_id: "1-1".into(), + source: Some("herdr:pi".into()), + seq: Some(42), + }), + }; + + let json = serde_json::to_string(&request).unwrap(); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn request_round_trips_for_pane_release_agent() { + let request = Request { + id: "req_release".into(), + method: Method::PaneReleaseAgent(PaneReleaseAgentParams { + pane_id: "1-1".into(), + source: "herdr:pi".into(), + agent: "pi".into(), + seq: Some(42), + }), + }; + + let json = serde_json::to_string(&request).unwrap(); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn request_uses_dot_method_names() { + let request = Request { + id: "req_1".into(), + method: Method::WorkspaceCreate(WorkspaceCreateParams { + cwd: Some("/tmp".into()), + focus: true, + label: Some("api".into()), + env: Default::default(), + }), + }; + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "workspace.create"); +} + +#[test] +fn request_round_trips_for_server_stop() { + let request = Request { + id: "req_stop".into(), + method: Method::ServerStop(EmptyParams::default()), + }; + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "server.stop"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn request_round_trips_for_server_reload_config() { + let request = Request { + id: "req_reload".into(), + method: Method::ServerReloadConfig(EmptyParams::default()), + }; + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "server.reload_config"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn request_round_trips_for_server_reload_agent_manifests() { + let request = Request { + id: "req_reload_agent_manifests".into(), + method: Method::ServerReloadAgentManifests(EmptyParams::default()), + }; + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "server.reload_agent_manifests"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn request_round_trips_for_server_agent_manifests() { + let request = Request { + id: "req_agent_manifests".into(), + method: Method::ServerAgentManifests(EmptyParams::default()), + }; + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "server.agent_manifests"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn request_round_trips_for_agent_explain() { + let request = Request { + id: "req_agent_explain".into(), + method: Method::AgentExplain(AgentTarget { + target: "agent-1".into(), + }), + }; + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "agent.explain"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn notification_show_request_parses() { + let json = r#"{"id":"req_1","method":"notification.show","params":{"title":"build failed","body":"api workspace","position":"top-left","sound":"request"}}"#; + let request: Request = serde_json::from_str(json).unwrap(); + let Method::NotificationShow(params) = request.method else { + panic!("wrong method parsed"); + }; + assert_eq!(params.title, "build failed"); + assert_eq!(params.body.as_deref(), Some("api workspace")); + assert_eq!( + params.position, + Some(crate::config::ToastHerdrPosition::TopLeft) + ); + assert_eq!(params.sound, NotificationShowSound::Request); +} + +#[test] +fn notification_show_sound_defaults_to_none() { + let json = r#"{"id":"req_1","method":"notification.show","params":{"title":"build failed"}}"#; + let request: Request = serde_json::from_str(json).unwrap(); + let Method::NotificationShow(params) = request.method else { + panic!("wrong method parsed"); + }; + + assert_eq!(params.sound, NotificationShowSound::None); +} + +#[test] +fn client_window_title_requests_round_trip() { + let set = Request { + id: "req_title_set".into(), + method: Method::ClientWindowTitleSet(ClientWindowTitleSetParams { + title: "herdr api".into(), + }), + }; + let json = serde_json::to_value(&set).unwrap(); + assert_eq!(json["method"], "client.window_title.set"); + assert_eq!(json["params"]["title"], "herdr api"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, set); + + let clear = Request { + id: "req_title_clear".into(), + method: Method::ClientWindowTitleClear(EmptyParams::default()), + }; + let json = serde_json::to_value(&clear).unwrap(); + assert_eq!(json["method"], "client.window_title.clear"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, clear); +} + +#[test] +fn unknown_method_is_rejected() { + let json = r#"{"id":"req_1","method":"nope","params":{}}"#; + let err = serde_json::from_str::<Request>(json) + .unwrap_err() + .to_string(); + assert!(err.contains("unknown variant")); +} + +#[test] +fn missing_required_params_are_rejected() { + let json = r#"{"id":"req_1","method":"pane.send_text","params":{"pane_id":"p_1"}}"#; + let err = serde_json::from_str::<Request>(json) + .unwrap_err() + .to_string(); + assert!(err.contains("text")); +} + +#[test] +fn pane_send_input_defaults_to_empty_text_and_keys() { + let json = r#" + { + "id": "req_1", + "method": "pane.send_input", + "params": { + "pane_id": "p_1" + } + } + "#; + + let request: Request = serde_json::from_str(json).unwrap(); + let Method::PaneSendInput(params) = request.method else { + panic!("wrong method parsed"); + }; + assert_eq!(params.pane_id, "p_1"); + assert!(params.text.is_empty()); + assert!(params.keys.is_empty()); +} + +#[test] +fn pane_wait_for_output_defaults_strip_ansi_to_true() { + let json = r#" + { + "id": "req_1", + "method": "pane.wait_for_output", + "params": { + "pane_id": "p_1", + "source": "recent", + "match": { "type": "substring", "value": "ready" } + } + } + "#; + + let request: Request = serde_json::from_str(json).unwrap(); + let Method::PaneWaitForOutput(params) = request.method else { + panic!("wrong method parsed"); + }; + assert!(params.strip_ansi); +} + +#[test] +fn pane_read_defaults_to_text_format() { + let json = r#" + { + "id": "req_1", + "method": "pane.read", + "params": { + "pane_id": "p_1", + "source": "visible" + } + } + "#; + + let request: Request = serde_json::from_str(json).unwrap(); + let Method::PaneRead(params) = request.method else { + panic!("wrong method parsed"); + }; + assert_eq!(params.format, ReadFormat::Text); +} + +#[test] +fn pane_current_request_round_trips() { + let request = Request { + id: "req_current".into(), + method: Method::PaneCurrent(PaneCurrentParams { + caller_pane_id: Some("w1-1".into()), + }), + }; + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "pane.current"); + assert_eq!(json["params"]["caller_pane_id"], "w1-1"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, request); +} + +#[test] +fn pane_process_info_request_round_trips() { + let request = Request { + id: "req_process_info".into(), + method: Method::PaneProcessInfo(PaneProcessInfoParams { + pane_id: Some("w1-1".into()), + }), + }; + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "pane.process_info"); + assert_eq!(json["params"]["pane_id"], "w1-1"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, request); +} + +#[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 json = serde_json::to_string(&event).unwrap(); + let restored: EventEnvelope = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, event); +} + +#[test] +fn subscribe_request_parses_parameterized_subscriptions() { + let json = r#" + { + "id": "sub_1", + "method": "events.subscribe", + "params": { + "subscriptions": [ + { + "type": "pane.output_matched", + "pane_id": "p_1_1", + "source": "recent", + "lines": 200, + "match": { "type": "substring", "value": "auth: received" } + }, + { + "type": "pane.agent_status_changed", + "pane_id": "p_1_1", + "agent_status": "done" + } + ] + } + } + "#; + + let request: Request = serde_json::from_str(json).unwrap(); + let Method::EventsSubscribe(params) = request.method else { + panic!("wrong method parsed"); + }; + assert_eq!(params.subscriptions.len(), 2); + assert!(matches!( + ¶ms.subscriptions[0], + Subscription::PaneOutputMatched { + pane_id, + source: ReadSource::Recent, + lines: Some(200), + r#match: OutputMatch::Substring { value }, + strip_ansi: true, + } if pane_id == "p_1_1" && value == "auth: received" + )); + assert!(matches!( + ¶ms.subscriptions[1], + Subscription::PaneAgentStatusChanged { + pane_id, + agent_status: Some(AgentStatus::Done), + } if pane_id == "p_1_1" + )); +} + +#[test] +fn subscription_event_envelope_round_trips() { + let event = SubscriptionEventEnvelope { + event: SubscriptionEventKind::PaneOutputMatched, + data: SubscriptionEventData::PaneOutputMatched(PaneOutputMatchedEvent { + pane_id: "p_1_1".into(), + matched_line: "auth: received".into(), + read: PaneReadResult { + pane_id: "p_1_1".into(), + workspace_id: "w_1".into(), + tab_id: "t_1_1".into(), + source: ReadSource::Recent, + format: ReadFormat::Text, + text: "auth: received\n".into(), + revision: 0, + truncated: false, + }, + }), + }; + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("\"event\":\"pane.output_matched\"")); + let restored: SubscriptionEventEnvelope = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, event); +} + +#[test] +fn success_response_round_trips() { + let response = SuccessResponse { + id: "req_1".into(), + result: ResponseResult::Pong { + version: "0.1.2".into(), + protocol: 6, + capabilities: Some(ServerCapabilities { live_handoff: true }), + }, + }; + + let json = serde_json::to_string(&response).unwrap(); + let restored: SuccessResponse = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, response); +} + +#[test] +fn worktree_request_and_response_round_trip() { + let request = Request { + id: "req_worktree".into(), + method: Method::WorktreeCreate(WorktreeCreateParams { + workspace_id: Some("1".into()), + branch: Some("worktree/api".into()), + base: Some("HEAD".into()), + focus: true, + ..WorktreeCreateParams::default() + }), + }; + let json = serde_json::to_string(&request).unwrap(); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, request); + + let response = SuccessResponse { + id: "req_worktree".into(), + result: ResponseResult::WorktreeCreated { + workspace: WorkspaceInfo { + workspace_id: "w_1".into(), + number: 2, + label: "herdr".into(), + focused: true, + pane_count: 1, + tab_count: 1, + active_tab_id: "w_1:1".into(), + agent_status: AgentStatus::Unknown, + worktree: Some(WorkspaceWorktreeInfo { + repo_key: "/repo/herdr/.git".into(), + repo_name: "herdr".into(), + repo_root: "/repo/herdr".into(), + checkout_path: "/worktrees/herdr/worktree-api".into(), + is_linked_worktree: true, + }), + }, + tab: TabInfo { + tab_id: "w_1:1".into(), + workspace_id: "w_1".into(), + number: 1, + label: "herdr".into(), + focused: true, + pane_count: 1, + agent_status: AgentStatus::Unknown, + }, + root_pane: PaneInfo { + pane_id: "w_1-1".into(), + terminal_id: "term_1".into(), + workspace_id: "w_1".into(), + tab_id: "w_1:1".into(), + focused: true, + cwd: Some("/worktrees/herdr/worktree-api".into()), + foreground_cwd: None, + label: None, + agent: None, + title: None, + display_agent: None, + agent_status: AgentStatus::Unknown, + custom_status: None, + state_labels: HashMap::new(), + agent_session: None, + revision: 0, + }, + worktree: WorktreeInfo { + path: "/worktrees/herdr/worktree-api".into(), + branch: Some("worktree/api".into()), + is_bare: false, + is_detached: false, + is_prunable: false, + is_linked_worktree: true, + open_workspace_id: Some("w_1".into()), + label: "herdr".into(), + }, + }, + }; + let json = serde_json::to_string(&response).unwrap(); + assert!(json.contains("\"type\":\"worktree_created\"")); + assert!(json.contains("\"worktree\"")); + let restored: SuccessResponse = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, response); +} + +#[test] +fn worktree_lifecycle_events_round_trip() { + let subscription = Request { + id: "sub_worktrees".into(), + method: Method::EventsSubscribe(EventsSubscribeParams { + subscriptions: vec![ + Subscription::WorktreeCreated {}, + Subscription::WorktreeOpened {}, + Subscription::WorktreeRemoved {}, + ], + }), + }; + let json = serde_json::to_string(&subscription).unwrap(); + assert!(json.contains("\"type\":\"worktree.created\"")); + assert!(json.contains("\"type\":\"worktree.opened\"")); + assert!(json.contains("\"type\":\"worktree.removed\"")); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, subscription); + + let workspace = WorkspaceInfo { + workspace_id: "w_2".into(), + number: 2, + label: "herdr".into(), + focused: true, + pane_count: 1, + tab_count: 1, + active_tab_id: "w_2:1".into(), + agent_status: AgentStatus::Unknown, + worktree: Some(WorkspaceWorktreeInfo { + repo_key: "/repo/herdr/.git".into(), + repo_name: "herdr".into(), + repo_root: "/repo/herdr".into(), + checkout_path: "/worktrees/herdr/worktree-api".into(), + is_linked_worktree: true, + }), + }; + let worktree = WorktreeInfo { + path: "/worktrees/herdr/worktree-api".into(), + branch: Some("worktree/api".into()), + is_bare: false, + is_detached: false, + is_prunable: false, + is_linked_worktree: true, + open_workspace_id: Some("w_2".into()), + label: "herdr".into(), + }; + + for event in [ + EventEnvelope { + event: EventKind::WorktreeCreated, + data: EventData::WorktreeCreated { + workspace: workspace.clone(), + worktree: worktree.clone(), + }, + }, + EventEnvelope { + event: EventKind::WorktreeOpened, + data: EventData::WorktreeOpened { + workspace: workspace.clone(), + worktree: worktree.clone(), + already_open: false, + }, + }, + EventEnvelope { + event: EventKind::WorktreeRemoved, + data: EventData::WorktreeRemoved { + workspace_id: "w_2".into(), + worktree: WorktreeInfo { + open_workspace_id: None, + ..worktree.clone() + }, + forced: false, + }, + }, + EventEnvelope { + event: EventKind::WorkspaceClosed, + data: EventData::WorkspaceClosed { + workspace_id: "w_2".into(), + workspace: Some(workspace.clone()), + }, + }, + ] { + let json = serde_json::to_string(&event).unwrap(); + let restored: EventEnvelope = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, event); + } +} + +#[test] +fn plugin_link_list_unlink_round_trip() { + let link = Request { + id: "plugin_link".into(), + method: Method::PluginLink(PluginLinkParams { + path: "/plugins/worktree-bootstrap".into(), + enabled: true, + source: None, + }), + }; + let json = serde_json::to_string(&link).unwrap(); + assert!(json.contains("\"method\":\"plugin.link\"")); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, link); + + let list = Request { + id: "plugin_list".into(), + method: Method::PluginList(PluginListParams { + plugin_id: Some("example.worktree-bootstrap".into()), + }), + }; + let json = serde_json::to_string(&list).unwrap(); + assert!(json.contains("\"method\":\"plugin.list\"")); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, list); + + let unlink = Request { + id: "plugin_unlink".into(), + method: Method::PluginUnlink(PluginUnlinkParams { + plugin_id: "example.worktree-bootstrap".into(), + }), + }; + let json = serde_json::to_string(&unlink).unwrap(); + assert!(json.contains("\"method\":\"plugin.unlink\"")); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, unlink); + + let plugin = InstalledPluginInfo { + plugin_id: "example.worktree-bootstrap".into(), + name: "Worktree Bootstrap".into(), + version: "0.1.0".into(), + description: Some("Prepare new worktrees".into()), + manifest_path: "/plugins/worktree-bootstrap/herdr-plugin.toml".into(), + plugin_root: "/plugins/worktree-bootstrap".into(), + enabled: true, + platforms: None, + build: vec![PluginManifestBuild { + platforms: None, + command: vec!["bun".into(), "install".into()], + }], + actions: vec![PluginManifestAction { + id: "bootstrap".into(), + title: "Bootstrap worktree".into(), + description: None, + contexts: vec![PluginActionContext::Workspace], + platforms: None, + command: vec!["bun".into(), "run".into(), "bootstrap.ts".into()], + }], + events: vec![PluginManifestEventHook { + on: "worktree.created".into(), + platforms: None, + command: vec!["bun".into(), "run".into(), "bootstrap.ts".into()], + }], + panes: vec![PluginManifestPane { + id: "board".into(), + title: "Board".into(), + description: None, + platforms: None, + placement: PluginPanePlacement::Overlay, + command: vec!["bun".into(), "run".into(), "board.ts".into()], + }], + link_handlers: vec![PluginManifestLinkHandler { + id: "github-pr".into(), + title: "Open GitHub PR".into(), + pattern: "^https://github.com/[^/]+/[^/]+/(issues|pull)/[0-9]+$".into(), + action: "bootstrap".into(), + platforms: None, + }], + source: Default::default(), + warnings: vec![], + }; + + for response in [ + SuccessResponse { + id: "plugin_link".into(), + result: ResponseResult::PluginLinked { + plugin: plugin.clone(), + }, + }, + SuccessResponse { + id: "plugin_list".into(), + result: ResponseResult::PluginList { + plugins: vec![plugin.clone()], + }, + }, + SuccessResponse { + id: "plugin_unlink".into(), + result: ResponseResult::PluginUnlinked { + plugin_id: plugin.plugin_id.clone(), + removed: true, + }, + }, + ] { + let json = serde_json::to_string(&response).unwrap(); + let restored: SuccessResponse = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, response); + } +} + +#[test] +fn layout_export_apply_round_trip() { + let root = LayoutNode::Split { + direction: SplitDirection::Right, + ratio: 0.6, + first: Box::new(LayoutNode::Pane { + pane: LayoutPane { + label: Some("editor".into()), + cwd: Some("/repo".into()), + ..Default::default() + }, + }), + second: Box::new(LayoutNode::Pane { + pane: LayoutPane { + label: Some("tests".into()), + command: Some(vec!["sh".into(), "-c".into(), "just test".into()]), + env: HashMap::from([("HERDR_ROLE".into(), "tests".into())]), + ..Default::default() + }, + }), + }; + + let export = Request { + id: "layout_export".into(), + method: Method::LayoutExport(LayoutExportParams { + tab_id: Some("w1:1".into()), + pane_id: None, + }), + }; + let json = serde_json::to_string(&export).unwrap(); + assert!(json.contains("\"method\":\"layout.export\"")); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, export); + + let apply = Request { + id: "layout_apply".into(), + method: Method::LayoutApply(LayoutApplyParams { + workspace_id: Some("w1".into()), + tab_id: None, + tab_label: Some("dev".into()), + focus: true, + root: root.clone(), + }), + }; + let json = serde_json::to_string(&apply).unwrap(); + assert!(json.contains("\"method\":\"layout.apply\"")); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, apply); + + let response = SuccessResponse { + id: "layout_export".into(), + result: ResponseResult::LayoutExport { + layout: LayoutDescription { + workspace_id: "w1".into(), + tab_id: "w1:1".into(), + zoomed: false, + focused_pane_id: "w1-1".into(), + root, + }, + }, + }; + let json = serde_json::to_string(&response).unwrap(); + let restored: SuccessResponse = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, response); +} + +#[test] +fn create_response_round_trips_with_root_pane() { + let response = SuccessResponse { + id: "req_2".into(), + result: ResponseResult::TabCreated { + tab: TabInfo { + tab_id: "w_1:2".into(), + workspace_id: "w_1".into(), + number: 2, + label: "review".into(), + focused: false, + pane_count: 1, + agent_status: AgentStatus::Unknown, + }, + root_pane: PaneInfo { + pane_id: "w_1-3".into(), + terminal_id: "term_example".into(), + workspace_id: "w_1".into(), + tab_id: "w_1:2".into(), + focused: false, + cwd: Some("/tmp/review".into()), + foreground_cwd: None, + label: None, + agent: None, + title: None, + display_agent: None, + agent_status: AgentStatus::Unknown, + custom_status: None, + state_labels: HashMap::new(), + agent_session: None, + revision: 0, + }, + }, + }; + + let json = serde_json::to_string(&response).unwrap(); + assert!(json.contains("\"type\":\"tab_created\"")); + assert!(json.contains("\"root_pane\"")); + let restored: SuccessResponse = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, response); +} + +#[test] +fn error_response_round_trips() { + let response = ErrorResponse { + id: "req_1".into(), + error: ErrorBody { + code: "pane_not_found".into(), + message: "pane p_1 not found".into(), + }, + }; + + let json = serde_json::to_string(&response).unwrap(); + let restored: ErrorResponse = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, response); +} + +#[test] +fn event_wait_parses_typed_match() { + let json = r#" + { + "id": "req_9", + "method": "events.wait", + "params": { + "match_event": { + "event": "pane_agent_status_changed", + "pane_id": "p_1", + "agent_status": "done" + }, + "timeout_ms": 30000 + } + } + "#; + + let request: Request = serde_json::from_str(json).unwrap(); + let Method::EventsWait(params) = request.method else { + panic!("wrong method parsed"); + }; + assert_eq!( + params.match_event, + EventMatch::PaneAgentStatusChanged { + pane_id: "p_1".into(), + agent_status: AgentStatus::Done, + } + ); +} + +#[test] +fn plugin_action_list_and_invoke_round_trips() { + let list = Request { + id: "req_plugin_action_list".into(), + method: Method::PluginActionList(PluginActionListParams { + plugin_id: Some("example.issue-flow".into()), + }), + }; + let json = serde_json::to_value(&list).unwrap(); + assert_eq!(json["method"], "plugin.action.list"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, list); + + let invoke = Request { + id: "req_plugin_action_invoke".into(), + method: Method::PluginActionInvoke(PluginActionInvokeParams { + plugin_id: Some("example.issue-flow".into()), + action_id: "assign-issue".into(), + context: None, + }), + }; + let json = serde_json::to_value(&invoke).unwrap(); + assert_eq!(json["method"], "plugin.action.invoke"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, invoke); + + let action_info = PluginActionInfo { + plugin_id: "example.issue-flow".into(), + action_id: "assign-issue".into(), + title: "Assign Issue".into(), + description: Some("Open the issue assignment UI".into()), + contexts: vec![PluginActionContext::Workspace, PluginActionContext::Pane], + command: vec!["assign".into(), "--issue".into()], + platforms: Some(vec![PluginPlatform::Linux, PluginPlatform::Macos]), + }; + assert_eq!( + action_info.qualified_id(), + "example.issue-flow.assign-issue" + ); + let json = serde_json::to_string(&action_info).unwrap(); + let restored: PluginActionInfo = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, action_info); +} + +#[test] +fn plugin_pane_open_request_round_trips() { + let request = Request { + id: "req_plugin_pane".into(), + method: Method::PluginPaneOpen(PluginPaneOpenParams { + plugin_id: "example.board".into(), + entrypoint: "board".into(), + placement: Some(PluginPanePlacement::Zoomed), + workspace_id: None, + target_pane_id: Some("1-1".into()), + direction: Some(SplitDirection::Right), + cwd: Some("/tmp".into()), + focus: true, + env: [("HERDR_ROLE".to_string(), "board".to_string())].into(), + }), + }; + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["method"], "plugin.pane.open"); + assert_eq!(json["params"]["env"]["HERDR_ROLE"], "board"); + let restored: Request = serde_json::from_value(json).unwrap(); + assert_eq!(restored, request); +} |
