diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-02 02:45:20 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-02 02:45:20 +0300 |
| commit | cc88b3b8e5bb9f7d9f23ed6ae85a52fd7b5b9ed6 (patch) | |
| tree | 4d37c760099070f1a1488b61c7b7271875aafef8 /tests | |
| parent | 18e69891dca486d669a584facd80644bb51f54a2 (diff) | |
feat: add stable client endpoint compatibility (#3509)
* feat: add stable client endpoint compatibility
* fix: keep portable-pty patch checks composable
* ci: run maintenance checks in pull requests
* fix: reject endpoint errors from terminal clients
* feat: keep compatible servers running across updates
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/cli/sessions.rs | 28 | ||||
| -rw-r--r-- | tests/client_mode.rs | 5 | ||||
| -rw-r--r-- | tests/cross_area.rs | 7 | ||||
| -rw-r--r-- | tests/detach_reattach.rs | 2 | ||||
| -rw-r--r-- | tests/fixtures/endpoint-hello-v1.json | 25 | ||||
| -rw-r--r-- | tests/fixtures/endpoint-method-shapes-v1.json | 39 | ||||
| -rw-r--r-- | tests/fixtures/endpoint-snapshot-v1.json | 106 | ||||
| -rw-r--r-- | tests/fixtures/endpoint-welcome-v1.json | 11 | ||||
| -rw-r--r-- | tests/live_handoff.rs | 71 | ||||
| -rw-r--r-- | tests/multi_client.rs | 3 | ||||
| -rw-r--r-- | tests/support/mod.rs | 97 |
11 files changed, 332 insertions, 62 deletions
diff --git a/tests/cli/sessions.rs b/tests/cli/sessions.rs index cc8474eb..8e5fc48f 100644 --- a/tests/cli/sessions.rs +++ b/tests/cli/sessions.rs @@ -398,7 +398,11 @@ fn status_commands_report_client_and_server_versions() { "stdout: {full_stdout}" ); assert!( - full_stdout.contains(" compatible: yes"), + full_stdout.contains(" private_protocol_compatible: yes"), + "stdout: {full_stdout}" + ); + assert!( + full_stdout.contains(" endpoint_compatible: yes"), "stdout: {full_stdout}" ); assert!( @@ -406,6 +410,10 @@ fn status_commands_report_client_and_server_versions() { "stdout: {full_stdout}" ); assert!( + full_stdout.contains(" server_binary_stale: no"), + "stdout: {full_stdout}" + ); + assert!( full_stdout.contains(&socket_path.display().to_string()), "stdout: {full_stdout}" ); @@ -422,7 +430,7 @@ fn status_commands_report_client_and_server_versions() { "stdout: {server_stdout}" ); assert!( - server_stdout.contains("protocol: 22"), + server_stdout.contains("private_protocol: 22"), "stdout: {server_stdout}" ); @@ -438,6 +446,10 @@ fn status_commands_report_client_and_server_versions() { "stdout: {client_stdout}" ); assert!( + client_stdout.contains("endpoint_protocol_generation: 1"), + "stdout: {client_stdout}" + ); + assert!( client_stdout.contains("binary: "), "stdout: {client_stdout}" ); @@ -445,25 +457,31 @@ 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"], 22); + assert_eq!(full_json["client"]["endpoint_protocol_generation"], 1); assert_eq!(full_json["server"]["status"], "running"); assert_eq!(full_json["server"]["running"], true); assert_eq!(full_json["server"]["compatible"], true); + assert_eq!(full_json["server"]["endpoint_compatible"], true); assert_eq!( full_json["server"]["socket"], socket_path.display().to_string() ); assert_eq!(full_json["server"]["restart_needed"], false); + assert_eq!(full_json["server"]["server_binary_stale"], false); assert_eq!(full_json["update"]["restart_needed"], false); + assert_eq!(full_json["update"]["server_binary_stale"], false); 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"], 22); assert_eq!(server_json["compatible"], true); + assert_eq!(server_json["endpoint_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"], 22); + assert_eq!(client_json["endpoint_protocol_generation"], 1); assert!(client_json["binary"] .as_str() .is_some_and(|path| !path.is_empty())); @@ -485,6 +503,10 @@ fn status_reports_not_running_when_server_socket_is_missing() { assert!(stdout.contains(" status: not running"), "stdout: {stdout}"); assert!(stdout.contains(" restart_needed: no"), "stdout: {stdout}"); assert!( + stdout.contains(" server_binary_stale: no"), + "stdout: {stdout}" + ); + assert!( stdout.contains(&socket_path.display().to_string()), "stdout: {stdout}" ); @@ -497,7 +519,9 @@ fn status_reports_not_running_when_server_socket_is_missing() { socket_path.display().to_string() ); assert_eq!(status_json["server"]["restart_needed"], false); + assert_eq!(status_json["server"]["server_binary_stale"], false); assert_eq!(status_json["update"]["restart_needed"], false); + assert_eq!(status_json["update"]["server_binary_stale"], false); cleanup_test_base(&base); } diff --git a/tests/client_mode.rs b/tests/client_mode.rs index c6c76c34..351c5fa2 100644 --- a/tests/client_mode.rs +++ b/tests/client_mode.rs @@ -18,8 +18,9 @@ use support::{ cleanup_test_base, client_shell_handshake, read_server_message, register_runtime_dir, register_spawned_herdr_pid, unregister_spawned_herdr_pid, wait_for_client_shell_bootstrap, wait_for_message_variant, wait_for_message_variants, wait_for_socket, wait_until, - CURRENT_PROTOCOL, SERVER_MESSAGE_PANE_SURFACE, SERVER_MESSAGE_PANE_SURFACE_PATCH, - SERVER_MESSAGE_SEMANTIC_NOTIFICATION, SERVER_MESSAGE_SERVER_SHUTDOWN, + CURRENT_ENDPOINT_PROTOCOL_GENERATION as CURRENT_PROTOCOL, SERVER_MESSAGE_PANE_SURFACE, + SERVER_MESSAGE_PANE_SURFACE_PATCH, SERVER_MESSAGE_SEMANTIC_NOTIFICATION, + SERVER_MESSAGE_SERVER_SHUTDOWN, }; fn unique_test_dir() -> PathBuf { diff --git a/tests/cross_area.rs b/tests/cross_area.rs index 9b856cb8..91c17800 100644 --- a/tests/cross_area.rs +++ b/tests/cross_area.rs @@ -14,8 +14,9 @@ use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize} use serde_json::{json, Value}; use support::{ cleanup_test_base, client_shell_handshake, register_runtime_dir, register_spawned_herdr_pid, - unregister_spawned_herdr_pid, CURRENT_PROTOCOL, SERVER_MESSAGE_CLIENT_SHELL_SNAPSHOT, - SERVER_MESSAGE_PANE_SURFACE, SERVER_MESSAGE_PANE_SURFACE_PATCH, + unregister_spawned_herdr_pid, CURRENT_ENDPOINT_PROTOCOL_GENERATION as CURRENT_PROTOCOL, + SERVER_MESSAGE_ENDPOINT_CONTROL, SERVER_MESSAGE_PANE_SURFACE, + SERVER_MESSAGE_PANE_SURFACE_PATCH, }; fn unique_test_dir() -> PathBuf { @@ -444,7 +445,7 @@ fn wait_for_frame(stream: &mut UnixStream, timeout: Duration) -> bool { let slice = deadline.saturating_duration_since(Instant::now()); match read_server_variant(stream, slice) { Ok(SERVER_MESSAGE_PANE_SURFACE | SERVER_MESSAGE_PANE_SURFACE_PATCH) => return true, - Ok(SERVER_MESSAGE_CLIENT_SHELL_SNAPSHOT) => {} + Ok(SERVER_MESSAGE_ENDPOINT_CONTROL) => {} Ok(_) => {} Err(err) if is_timeout(&err) => {} Err(_) => return false, diff --git a/tests/detach_reattach.rs b/tests/detach_reattach.rs index f570e979..cdf0736c 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_shell_handshake, drain_messages, register_runtime_dir, register_spawned_herdr_pid, send_detach, unregister_spawned_herdr_pid, wait_for_disconnect, - wait_for_socket, wait_until, CURRENT_PROTOCOL, + wait_for_socket, wait_until, CURRENT_ENDPOINT_PROTOCOL_GENERATION as CURRENT_PROTOCOL, }; const CUSTOM_HEADLESS_SIZE_CONFIG: &str = r#"onboarding = false diff --git a/tests/fixtures/endpoint-hello-v1.json b/tests/fixtures/endpoint-hello-v1.json new file mode 100644 index 00000000..dfe7ebb3 --- /dev/null +++ b/tests/fixtures/endpoint-hello-v1.json @@ -0,0 +1,25 @@ +{ + "generation": 1, + "cell_width_px": 8, + "cell_height_px": 16, + "surface_size": { + "cols": 80, + "rows": 24 + }, + "pixel_mouse": true, + "direct_graphics": false, + "endpoint_keybindings": false, + "mouse_capture": true, + "snapshot_codecs": [ + "shell.snapshot.v1" + ], + "surface_codecs": [ + "shell.surface.v1" + ], + "input_codecs": [ + "shell.input.semantic.v1" + ], + "blob_codecs": [ + "shell.blob.v1" + ] +} diff --git a/tests/fixtures/endpoint-method-shapes-v1.json b/tests/fixtures/endpoint-method-shapes-v1.json new file mode 100644 index 00000000..b473869f --- /dev/null +++ b/tests/fixtures/endpoint-method-shapes-v1.json @@ -0,0 +1,39 @@ +{ + "command.invoke": "b16817652442b1ea2405e60a0443826a55baca461e938f524e79b16a675c5899", + "integration.install": "913055b838166650fee6713332c1cc436efd4f8fae9a2064d8f9b6616a7a66f8", + "integration.list": "60c57189adf9c281958062f0e11680bda0ece83307c506fa18bff8b01ebfb4e8", + "layout.set_split_ratio": "13ef71d865f49c4f512fdfa8de64e1bdc0d4299675f78daf61bd5cdc10174d2f", + "pane.close": "dcc277a6a4f9b8fc3438d8fa61de2688f4338b0d2d60100798e4103690e0b316", + "pane.copy_motion": "57e690b444d5dbb408ac3e0d40ff307f77d6e79dccd3c026cd19580e1bd78983", + "pane.copy_search": "5391364ee0a0a12776a5ba3c59e7ca2a879448ec79ff357c8157b0c01e3cbfc1", + "pane.edit_scrollback": "c9b4722bb38e280f97915264205ba19171d76027002e7eeefae19141a44a91de", + "pane.focus": "680c1dffa1466d7725550f91cb58af6e1d45dcbde2bf5d4765e5dc5fc9c982d5", + "pane.focus_direction": "4e34a76f69213378d51b397927edc43e098c9cd24660fdcbf8b5001b6e226e05", + "pane.input.set": "4eb8914f4cd40417ba5631d4e9058a04cde11191c58c08e66553f43e4ca0ffd8", + "pane.link.activate": "42bf791ae426cb56f3fdad63e861bfe01432cb8e81e046960a9245d89dc442ff", + "pane.rename": "cc3df3226f618ec4b9a175eca3450a66a9f2bea9fa39843e1a5900a1b45918c3", + "pane.resize": "9fbcc70b8908c43f47b0ff85d0a9040ce18c9179a029ed7d8f2616a05d8c301d", + "pane.scroll": "3229d55230b89730e5231a43d87cefa98cfda24acaaf6365f7c6a3548cc42db1", + "pane.selection.read": "2f7b0fdf4f0fc1f1fe9ab4d962c833dd39a1e9bd866d77bbf0031f8b2c485534", + "pane.split": "5d740322cac5287070194fb522c8135795a6f877b44652ea4959c63b4c67cefd", + "pane.swap": "3146d499d8174d73cfdfa3b6ca44f5d5d0951ab1cdd1808f94a3cf8846dc8057", + "pane.zoom": "0521e444ea892abb1f73d16dc0e10a5667c8a752284eae4cbc0961cd6d96bbfe", + "product_announcement.dismiss": "7ce099f799f396d4733c77ddfa0ac7e2fd597e49c52e2739af3b883624a834d5", + "release_notes.dismiss": "75e096dc234e7879b401b934097ecc3a4a91b78d89a909d47f6dd9f49cfc7bdb", + "server.reload_config": "6def1206dab9748df80e6c98ab6fbee3d45c1e5019e889dd8c9abd22a3dadac8", + "tab.close": "29e03e6723e4eeef762161db2f6dd23306432b9ecbb8c879efe1e7b1fe697312", + "tab.create": "c2477e9bada16b329303fc552d66b900ad5c38ae9ed4ea0884f4b3de8ff01820", + "tab.focus": "666eff9e6ae83ae1dd8fd10c456cc5d216466a667070ee3f9fdca7a4422611e6", + "tab.move": "63c354ed4684d3216fc7f712c3073989f757135293a5ee37f49a2734841f4187", + "tab.rename": "bd84404b569286748a8cfa68720aacdbbe549fc5d2b009529d26e8cd455ef071", + "workspace.close": "e4e762f28e3e2fb96be974f3cd9be7420ec7c57cf1b6f55c19d7c67853197618", + "workspace.create": "b9dc2df43776d7d73bea576ef31b25276604c42a1c91e66e5bde8715119936ac", + "workspace.focus": "39675f05cfe6110ece18aa2789c26f0905303b2114af801712857d0c021df150", + "workspace.move": "1d8d308b3a6f878ef36232c7776a6d78e9ffb05f0ff3bdb52476d3a71a98208a", + "workspace.move_block": "1c1fdf3cd9387152e09d676830998f961cf80143704940da4dc88b5c7cb639a9", + "workspace.rename": "0e00bc779c367ef3e7a154c63b9bbbf9b9bcaa9b198450f5c479f9b9956fa7cb", + "worktree.create": "e5cca6546ace513fa2f6ca48c2ec7bc3eebcd589feeb6b97b9d16fd415037399", + "worktree.list": "b0ee8d77a954fdd418f7719074b07484294ee1395cb4dc6e38ab831a5ffac000", + "worktree.open": "5ea3b41d74b095c12bf342cd5cde3f23e2e1b9c71281dfb7138e929484b26fae", + "worktree.remove": "49425bdf418edd0c7f95fc80f7060210e390d5f5f659efc9f47db749f3d9716b" +} diff --git a/tests/fixtures/endpoint-snapshot-v1.json b/tests/fixtures/endpoint-snapshot-v1.json new file mode 100644 index 00000000..001229a3 --- /dev/null +++ b/tests/fixtures/endpoint-snapshot-v1.json @@ -0,0 +1,106 @@ +{ + "boot_id": "boot-v1", + "revision": 7, + "config_diagnostic": "endpoint warning", + "product_announcement": { + "version": "1.0.0", + "id": "announcement-v1", + "title": "Announcement", + "body": "Body", + "preview": false + }, + "update_available": "1.0.1", + "update_install_command": "herdr update", + "server_keybindings_toml": "[keys]", + "latest_release_notes_available": true, + "integration_updates_available": true, + "worktree_directory": "/worktrees", + "release_notes": { + "version": "1.0.1", + "body": "Notes", + "preview": true + }, + "focused_workspace_id": "w1", + "focused_tab_id": "w1:t1", + "focused_pane_id": "w1:p1", + "tab_bar_right": [ + { + "text": "host", + "accent": true + } + ], + "tab_bar_right_separator": " | ", + "agent_view_label": "focus", + "agent_order": ["w1:p1"], + "workspaces": [ + { + "workspace_id": "w1", + "active_tab_id": "w1:t1", + "new_workspace_cwd": "/repo", + "number": 1, + "label": "repo", + "custom_label": false, + "branch": "main", + "git_ahead_behind": [1, 2], + "tokens": [["model", "opus"]], + "worktree": { + "key": "repo/main", + "label": "main", + "is_linked_worktree": true + }, + "focused": true, + "agent_status": "future_status_from_new_server" + } + ], + "tabs": [ + { + "tab_id": "w1:t1", + "workspace_id": "w1", + "number": 1, + "label": "main", + "custom_label": false, + "zoomed": false, + "focused": true, + "agent_status": "working" + } + ], + "panes": [ + { + "pane_id": "w1:p1", + "workspace_id": "w1", + "tab_id": "w1:t1", + "label": "shell", + "cwd": "/repo", + "foreground_cwd": "/repo", + "focused": true, + "right_click_passthrough": false + } + ], + "agents": [ + { + "pane_id": "w1:p1", + "workspace_id": "w1", + "tab_id": "w1:t1", + "name": "reviewer", + "display_agent": "Claude", + "agent": "claude", + "title": "Review", + "terminal_title": "Claude Review", + "terminal_title_stripped": "Claude Review", + "agent_status": "blocked", + "state_change_seq": 9, + "state_labels": [["blocked", "waiting"]], + "tokens": [["task", "review"]], + "focused": true + } + ], + "commands": [ + { + "command_id": "command-v1", + "binding_label": "prefix+x", + "binding_labels": ["prefix+x"], + "action": "Shell", + "description": "Command" + } + ] +} diff --git a/tests/fixtures/endpoint-welcome-v1.json b/tests/fixtures/endpoint-welcome-v1.json new file mode 100644 index 00000000..3d153dfa --- /dev/null +++ b/tests/fixtures/endpoint-welcome-v1.json @@ -0,0 +1,11 @@ +{ + "generation": 1, + "server_version": "0.8.2", + "snapshot_codec": "shell.snapshot.v1", + "surface_codec": "shell.surface.v1", + "input_codec": "shell.input.semantic.v1", + "blob_codec": "shell.blob.v1", + "methods": [ + "pane.focus" + ] +} diff --git a/tests/live_handoff.rs b/tests/live_handoff.rs index d58fb301..50b39698 100644 --- a/tests/live_handoff.rs +++ b/tests/live_handoff.rs @@ -14,7 +14,7 @@ use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize} use support::{ cleanup_test_base, client_shell_handshake, register_runtime_dir, register_spawned_herdr_pid, send_client_shell_shift_enter, unregister_spawned_herdr_pid, wait_for_client_shell_bootstrap, - wait_for_message_variant, wait_for_socket, SERVER_MESSAGE_CLIENT_SHELL_SNAPSHOT, + wait_for_message_variant, wait_for_socket, SERVER_MESSAGE_ENDPOINT_CONTROL, SERVER_MESSAGE_SERVER_SHUTDOWN, }; @@ -863,22 +863,17 @@ fn live_handoff_preserves_pane_process_io() { assert_eq!(unsafe { libc::kill(child_pid as libc::pid_t, 0) }, 0); assert_eq!(unsafe { libc::kill(second_child_pid as libc::pid_t, 0) }, 0); - let protocol = request( - &api_socket, - serde_json::json!({"id":"test:protocol","method":"ping","params":{}}), - )["result"]["protocol"] - .as_u64() - .unwrap() as u32; + let endpoint_generation = support::CURRENT_ENDPOINT_PROTOCOL_GENERATION; let mut client_stream = UnixStream::connect(&client_socket).unwrap(); - let (server_protocol, error) = - client_shell_handshake(&mut client_stream, protocol, 54, 23).unwrap(); - assert_eq!(server_protocol, protocol); + let (server_generation, error) = + client_shell_handshake(&mut client_stream, endpoint_generation, 54, 23).unwrap(); + assert_eq!(server_generation, endpoint_generation); assert!(error.is_none(), "client shell handshake failed: {error:?}"); assert!( wait_for_message_variant( &mut client_stream, Duration::from_secs(5), - SERVER_MESSAGE_CLIENT_SHELL_SNAPSHOT, + SERVER_MESSAGE_ENDPOINT_CONTROL, ) .unwrap(), "client shell should receive a complete snapshot before handoff" @@ -953,9 +948,17 @@ fn live_handoff_preserves_pane_process_io() { wait_for_output(&api_socket, &second_pane_id, "second:after-handoff-sec"); let mut reattached_shell = UnixStream::connect(&client_socket).unwrap(); - let (server_protocol, error) = - client_shell_handshake(&mut reattached_shell, protocol, 54, 23).unwrap(); - assert_eq!(server_protocol, protocol); + let (server_generation, error) = client_shell_handshake( + &mut reattached_shell, + support::CURRENT_ENDPOINT_PROTOCOL_GENERATION, + 54, + 23, + ) + .unwrap(); + assert_eq!( + server_generation, + support::CURRENT_ENDPOINT_PROTOCOL_GENERATION + ); assert!(error.is_none(), "reattached client shell failed: {error:?}"); wait_for_client_shell_bootstrap(&mut reattached_shell, Duration::from_secs(5)) .expect("fresh client shell should receive restored snapshot before pane content"); @@ -1030,12 +1033,6 @@ pathlib.Path({received:?}).write_text(data.hex()) )); support::wait_for_file(&ready_marker, Duration::from_secs(5)); - let protocol = request( - &api_socket, - serde_json::json!({"id":"test:protocol","method":"ping","params":{}}), - )["result"]["protocol"] - .as_u64() - .unwrap() as u32; assert_ok(request( &api_socket, serde_json::json!({"id":"test:handoff","method":"server.live_handoff","params":{}}), @@ -1045,9 +1042,17 @@ pathlib.Path({received:?}).write_text(data.hex()) wait_for_socket(&client_socket, Duration::from_secs(5)); let mut client_stream = UnixStream::connect(&client_socket).unwrap(); - let (server_protocol, error) = - client_shell_handshake(&mut client_stream, protocol, 54, 23).unwrap(); - assert_eq!(server_protocol, protocol); + let (server_generation, error) = client_shell_handshake( + &mut client_stream, + support::CURRENT_ENDPOINT_PROTOCOL_GENERATION, + 54, + 23, + ) + .unwrap(); + assert_eq!( + server_generation, + support::CURRENT_ENDPOINT_PROTOCOL_GENERATION + ); assert!(error.is_none(), "client shell handshake failed: {error:?}"); wait_for_client_shell_bootstrap(&mut client_stream, Duration::from_secs(5)) .expect("client shell should receive restored state before sending input"); @@ -1124,12 +1129,6 @@ pathlib.Path({received:?}).write_text(data.hex()) )); support::wait_for_file(&ready_marker, Duration::from_secs(5)); - let protocol = request( - &api_socket, - serde_json::json!({"id":"test:protocol","method":"ping","params":{}}), - )["result"]["protocol"] - .as_u64() - .unwrap() as u32; assert_ok(request( &api_socket, serde_json::json!({"id":"test:handoff","method":"server.live_handoff","params":{}}), @@ -1139,9 +1138,17 @@ pathlib.Path({received:?}).write_text(data.hex()) wait_for_socket(&client_socket, Duration::from_secs(5)); let mut client_stream = UnixStream::connect(&client_socket).unwrap(); - let (server_protocol, error) = - client_shell_handshake(&mut client_stream, protocol, 54, 23).unwrap(); - assert_eq!(server_protocol, protocol); + let (server_generation, error) = client_shell_handshake( + &mut client_stream, + support::CURRENT_ENDPOINT_PROTOCOL_GENERATION, + 54, + 23, + ) + .unwrap(); + assert_eq!( + server_generation, + support::CURRENT_ENDPOINT_PROTOCOL_GENERATION + ); assert!(error.is_none(), "client shell handshake failed: {error:?}"); wait_for_client_shell_bootstrap(&mut client_stream, Duration::from_secs(5)) .expect("client shell should receive restored state before sending input"); diff --git a/tests/multi_client.rs b/tests/multi_client.rs index 83fb0b00..ae642a22 100644 --- a/tests/multi_client.rs +++ b/tests/multi_client.rs @@ -16,7 +16,8 @@ use support::{ cleanup_test_base, client_shell_handshake, drain_messages, register_runtime_dir, register_spawned_herdr_pid, send_detach, unregister_spawned_herdr_pid, wait_for_client_shell_bootstrap, wait_for_message_variant, wait_for_message_variants, - CURRENT_PROTOCOL, SERVER_MESSAGE_PANE_SURFACE, SERVER_MESSAGE_PANE_SURFACE_PATCH, + CURRENT_ENDPOINT_PROTOCOL_GENERATION as CURRENT_PROTOCOL, SERVER_MESSAGE_PANE_SURFACE, + SERVER_MESSAGE_PANE_SURFACE_PATCH, }; fn unique_test_dir() -> PathBuf { diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 2c863fbe..68337ade 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -14,13 +14,14 @@ 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 = 22; +pub const CURRENT_ENDPOINT_PROTOCOL_GENERATION: u32 = 1; pub const SERVER_MESSAGE_SERVER_SHUTDOWN: u32 = 3; -pub const SERVER_MESSAGE_CLIENT_SHELL_SNAPSHOT: u32 = 12; +pub const SERVER_MESSAGE_ENDPOINT_CONTROL: u32 = 20; pub const SERVER_MESSAGE_PANE_SURFACE: u32 = 13; pub const SERVER_MESSAGE_SEMANTIC_NOTIFICATION: u32 = 14; pub const SERVER_MESSAGE_PANE_SURFACE_PATCH: u32 = 19; -const CLIENT_MESSAGE_CLIENT_SHELL_HELLO: u32 = 11; const CLIENT_MESSAGE_CLIENT_SHELL_PANE_INPUT: u32 = 13; +const CLIENT_MESSAGE_ENDPOINT_CONTROL: u32 = 20; pub fn register_spawned_herdr_pid(pid: Option<u32>) { let Some(pid) = pid else { @@ -185,6 +186,25 @@ fn encode_varint_enum(variant_idx: u32, fields: &[&[u8]]) -> Vec<u8> { buf } +fn encode_string(value: &str) -> Vec<u8> { + let mut encoded = encode_varint_u32(value.len() as u32); + encoded.extend_from_slice(value.as_bytes()); + encoded +} + +fn decode_string(payload: &[u8], offset: &mut usize) -> Result<String, String> { + let (len, consumed) = decode_varint_u32(payload, *offset)?; + *offset += consumed; + let len = len as usize; + if *offset + len > payload.len() { + return Err("payload too short for string content".into()); + } + let value = String::from_utf8(payload[*offset..*offset + len].to_vec()) + .map_err(|err| err.to_string())?; + *offset += len; + Ok(value) +} + fn decode_welcome(payload: &[u8]) -> Result<(u32, Option<String>), String> { let mut offset = 0; let (variant, consumed) = decode_varint_u32(payload, offset)?; @@ -225,10 +245,10 @@ fn decode_welcome(payload: &[u8]) -> Result<(u32, Option<String>), String> { Ok((version, error)) } -fn finish_handshake( +fn read_handshake_response( stream: &mut UnixStream, hello_payload: &[u8], -) -> Result<(u32, Option<String>), String> { +) -> Result<Vec<u8>, String> { stream .set_read_timeout(Some(Duration::from_secs(5))) .map_err(|e| e.to_string())?; @@ -245,7 +265,7 @@ fn finish_handshake( } let mut payload = vec![0u8; len]; stream.read_exact(&mut payload).map_err(|e| e.to_string())?; - decode_welcome(&payload) + Ok(payload) } pub fn client_handshake( @@ -265,30 +285,60 @@ pub fn client_handshake( &[0], // pixel_mouse = false ], ); - finish_handshake(stream, &hello_payload) + let response = read_handshake_response(stream, &hello_payload)?; + decode_welcome(&response) } pub fn client_shell_handshake( stream: &mut UnixStream, - version: u32, + endpoint_generation: u32, surface_cols: u16, surface_rows: u16, ) -> Result<(u32, Option<String>), String> { + let data = serde_json::json!({ + "generation": endpoint_generation, + "cell_width_px": 8, + "cell_height_px": 16, + "surface_size": {"cols": surface_cols, "rows": surface_rows}, + "pixel_mouse": false, + "direct_graphics": false, + "endpoint_keybindings": false, + "mouse_capture": false, + "snapshot_codecs": ["shell.snapshot.v1"], + "surface_codecs": ["shell.surface.v1"], + "input_codecs": ["shell.input.semantic.v1"], + "blob_codecs": ["shell.blob.v1"] + }) + .to_string(); let hello_payload = encode_varint_enum( - CLIENT_MESSAGE_CLIENT_SHELL_HELLO, - &[ - &encode_varint_u32(version), - &encode_varint_u32(8), - &encode_varint_u32(16), - &encode_varint_u16(surface_cols), - &encode_varint_u16(surface_rows), - &[0], // pixel mouse disabled - &[0], // direct graphics disabled - &[0], // client-owned keybindings - &[0], // mouse capture disabled - ], + CLIENT_MESSAGE_ENDPOINT_CONTROL, + &[&encode_string("endpoint.hello.v1"), &encode_string(&data)], ); - finish_handshake(stream, &hello_payload) + let response = read_handshake_response(stream, &hello_payload)?; + let mut offset = 0; + let (variant, consumed) = decode_varint_u32(&response, offset)?; + offset += consumed; + if variant != SERVER_MESSAGE_ENDPOINT_CONTROL { + return Err(format!( + "expected EndpointControl (variant {SERVER_MESSAGE_ENDPOINT_CONTROL}), got variant {variant}" + )); + } + let kind = decode_string(&response, &mut offset)?; + if kind != "endpoint.welcome.v1" { + return Err(format!("expected endpoint.welcome.v1, got {kind}")); + } + let data = decode_string(&response, &mut offset)?; + let value: serde_json::Value = serde_json::from_str(&data).map_err(|err| err.to_string())?; + let generation = value["generation"] + .as_u64() + .ok_or_else(|| "endpoint welcome omitted generation".to_owned())? + as u32; + let error = value["error"] + .as_object() + .and_then(|error| error.get("message")) + .and_then(serde_json::Value::as_str) + .map(str::to_owned); + Ok((generation, error)) } pub fn read_server_message(stream: &mut UnixStream) -> Result<(u32, Vec<u8>), String> { @@ -407,7 +457,12 @@ pub fn wait_for_client_shell_bootstrap( let mut saw_snapshot = false; while Instant::now() < deadline { match read_server_message(stream) { - Ok((SERVER_MESSAGE_CLIENT_SHELL_SNAPSHOT, _)) => saw_snapshot = true, + Ok((SERVER_MESSAGE_ENDPOINT_CONTROL, payload)) => { + let mut offset = 0; + if decode_string(&payload, &mut offset).as_deref() == Ok("shell.snapshot.v1") { + saw_snapshot = true; + } + } Ok((SERVER_MESSAGE_PANE_SURFACE, _)) if saw_snapshot => return Ok(()), Ok((SERVER_MESSAGE_PANE_SURFACE, _)) => { return Err("client shell pane surface arrived before its snapshot".into()); |
