merge: sidebar-cards/110-lenient-activity
This commit is contained in:
@@ -123,7 +123,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,
|
||||
@@ -137,6 +142,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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP `POST /api/sessions/:id/activity` body (#105): an agent's report of
|
||||
/// its own state. The daemon holds the reported state above its own inference
|
||||
/// for a fixed TTL, then resumes inference.
|
||||
@@ -658,6 +679,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 activity_detail_defaults_to_none_on_legacy_payload() {
|
||||
// #105: a daemon predating the detail field omits it; it must
|
||||
|
||||
Reference in New Issue
Block a user