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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user