From 71add0215a041102da2c38ddb039ab50d657ee45 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Mon, 24 Aug 2026 21:28:30 +0100 Subject: [PATCH] fix(maestro): deserialize unknown ActivityState variants as Idle A newer daemon's unknown activity variant failed the whole Session deserialization on older clients. Hand-written Deserialize maps any unknown string to Idle; non-string values still error. (#110) --- crates/maestro-protocol/src/lib.rs | 52 +++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/crates/maestro-protocol/src/lib.rs b/crates/maestro-protocol/src/lib.rs index e960175..9b370d1 100644 --- a/crates/maestro-protocol/src/lib.rs +++ b/crates/maestro-protocol/src/lib.rs @@ -117,7 +117,12 @@ pub enum SessionStatus { /// Inferred session activity, orthogonal to [`SessionStatus`] (NAP-340). The /// daemon derives this from tmux pane state, PTY quiescence, and `/proc` /// inspection — never by parsing the terminal byte stream. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +/// +/// Deserializes leniently (#110, SIDEBAR.md Precedence): an unknown variant +/// from a newer daemon becomes [`ActivityState::Idle`] instead of failing the +/// whole `Session`. Non-string values still error — only variant names are +/// forgiven. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)] pub enum ActivityState { /// Producing output, or the foreground process is busy. Active, @@ -131,6 +136,22 @@ pub enum ActivityState { Error, } +impl<'de> Deserialize<'de> for ActivityState { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(match s.as_str() { + "Active" => Self::Active, + "AwaitingInput" => Self::AwaitingInput, + "Error" => Self::Error, + // Known or not, any other spelling degrades to the neutral default + _ => Self::Idle, + }) + } +} + /// Optional worktree request attached to a `CreateSessionRequest`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct WorktreeSpec { @@ -639,6 +660,35 @@ mod tests { assert_eq!(ActivityState::default(), ActivityState::Idle); } + // #110 / SIDEBAR.md Precedence: a future daemon may send an + // `ActivityState` variant this client does not know; it must degrade to + // the neutral default instead of failing the whole `Session`. + #[test] + fn unknown_activity_state_variant_deserializes_to_idle() { + assert_eq!( + serde_json::from_value::(serde_json::json!("Teleported")).unwrap(), + ActivityState::Idle + ); + + let mut future = serde_json::to_value(sample_session()).unwrap(); + future["activity"] = serde_json::json!("QuantumEntangled"); + let s: Session = serde_json::from_value(future).unwrap(); + assert_eq!(s.activity, ActivityState::Idle); + } + + #[test] + fn non_string_activity_payload_still_fails_deserialization() { + let json = serde_json::json!({ + "id": Uuid::from_u128(1), + "cwd": "/tmp", + "command": "bash", + "status": "Running", + "created_at": "2026-05-24T12:00:00Z", + "activity": 42, + }); + assert!(serde_json::from_value::(json).is_err()); + } + #[test] fn session_round_trips() { let s = sample_session();