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)
This commit is contained in:
Miguel Palhas
2026-08-24 21:28:30 +01:00
parent 766d256970
commit 71add0215a
+51 -1
View File
@@ -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<D>(deserializer: D) -> Result<Self, D::Error>
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::<ActivityState>(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::<Session>(json).is_err());
}
#[test]
fn session_round_trips() {
let s = sample_session();