From 2ce2e696363fe131f9045a5dbf853627d02e1fa6 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:45:34 +0800 Subject: [PATCH] test(protocol): hold the wire structs an older peer sends short MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swept every `#[serde(default)]` in the protocol by removing it and running the suite: 56 fields, and most removals changed nothing, because serde defaults a missing `Option` on its own — verified against serde directly rather than assumed. The attribute is only load-bearing on a `Vec`, `String`, number or `bool`, where a missing field fails the whole frame instead of degrading. Thirteen structs have such a field. Three were held. This adds the four that carry the most traffic across a version boundary: `NativeSshSpec`, which goes to the daemon on every SSH connect and has eight of them; `SftpEntry`, which every remote listing is a page of; and the `ProcEntry`/`PortEntry` rows behind `tty7 procs`. Each is decoded from the JSON an older peer would send — required fields only — and each of the four attributes was removed to confirm the test fails without it. --- crates/tty7-core/src/daemon/protocol.rs | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/tty7-core/src/daemon/protocol.rs b/crates/tty7-core/src/daemon/protocol.rs index 5cf0a9d0..73cbfc1c 100644 --- a/crates/tty7-core/src/daemon/protocol.rs +++ b/crates/tty7-core/src/daemon/protocol.rs @@ -2322,6 +2322,44 @@ mod tests { assert_eq!(info.title, ""); } + /// The wire structs an older peer sends short still decode. + /// + /// `#[serde(default)]` on a non-`Option` field is load-bearing: serde + /// defaults a missing `Option` on its own, but a missing `Vec`, `String`, + /// number or `bool` fails the whole frame. Every one of these carries the + /// attribute because it arrived after its struct shipped, and dropping it + /// would refuse messages from any peer that predates it. + /// + /// Checked struct by struct with the JSON an older peer would send — + /// required fields only. Removing any of the attributes named below fails + /// here; before this, only `PaneInfo` and `DaemonVersion` were held. + #[test] + fn wire_structs_decode_from_a_peer_that_sends_only_the_old_fields() { + // Every SSH connect carries one of these to the daemon. + let spec: NativeSshSpec = + serde_json::from_str(r#"{"host":"h","port":22,"user":"u","auth_mode":"agent"}"#) + .expect("NativeSshSpec must decode without its later fields"); + assert!(spec.identity_files.is_empty()); + assert!(!spec.agent_forward); + assert!(spec.forwards.is_empty()); + + // Every remote directory listing is a page of these. + let entry: SftpEntry = serde_json::from_str(r#"{"name":"f","kind":"file"}"#) + .expect("SftpEntry must decode without its later fields"); + assert_eq!(entry.size, 0); + assert_eq!(entry.mtime, 0); + assert_eq!(entry.permissions, 0); + assert!(!entry.target_is_dir); + + // Process and port rows, read by `tty7 procs`. + let proc: ProcEntry = serde_json::from_str(r#"{"pid":1,"name":"sh","depth":0}"#) + .expect("ProcEntry must decode without its later fields"); + assert!(!proc.foreground); + let port: PortEntry = serde_json::from_str(r#"{"port":80,"pid":1,"name":"sh"}"#) + .expect("PortEntry must decode without its later fields"); + assert_eq!(port.addr, ""); + } + /// A `ShellSpec` from a peer that predates its later fields still decodes. /// /// This crosses the wire inside every `Spawn` that names a shell, in both