pulse-pixelstream-types 0.13.10

Shared Myko entity and command types for the Pulse Pixelstream recording cell.
Documentation
use myko::prelude::*;
use myko_macros::myko_item;

use crate::{FrameCapturePhase, FrameCaptureReceipt, FrameCaptureTarget, StoredCaptureContext};

/// Bridge-owned authoritative status for one stream's capture.
#[myko_item]
pub struct FrameCaptureStatus {
    pub streamer_id: String,
    #[serde(default)]
    pub capture_id: String,
    #[serde(default)]
    pub phase: FrameCapturePhase,
    /// Editorial context echoed back so the UI can label the result.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[ts(type = "unknown")]
    pub capture_context: Option<StoredCaptureContext>,
    #[serde(default)]
    pub target: FrameCaptureTarget,
    /// Generation the bridge actually captured, stamped from Cluster.
    #[serde(default)]
    pub observed_generation: String,
    /// Typed receipts, populated on Complete. Rendered verbatim.
    #[serde(default)]
    pub receipts: Vec<FrameCaptureReceipt>,
    /// Human-readable failure reason when phase is Failed. Cluster's exact
    /// wording, forwarded rather than reinterpreted.
    #[serde(default)]
    pub error: String,
    /// Set when this refusal is one a forced submit could bypass (missing or
    /// stalled frame-progress telemetry). Drives the explicit override the UI
    /// offers; identity checks are never bypassable.
    #[serde(default)]
    pub forceable: bool,
    #[serde(default)]
    pub updated_at_ms: u64,
}

impl FrameCaptureStatus {
    pub fn is_running(&self) -> bool {
        self.phase.is_running()
    }

    /// One-line summary for the chip tooltip.
    pub fn summary(&self) -> String {
        match self.phase {
            FrameCapturePhase::Idle => String::new(),
            FrameCapturePhase::Failed if !self.error.is_empty() => self.error.clone(),
            FrameCapturePhase::Complete => {
                format!("{} artifact(s) captured", self.receipts.len())
            }
            _ => format!("Frame capture on {}", self.target.cluster_name),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{FrameCapturePhase, FrameCaptureReceipt, FrameCaptureTarget};

    fn status_fixture() -> FrameCaptureStatus {
        FrameCaptureStatus {
            id: "render-13".to_owned().into(),
            streamer_id: "render-13".to_owned(),
            capture_id: "capture-1".to_owned(),
            phase: FrameCapturePhase::Idle,
            capture_context: None,
            target: FrameCaptureTarget::default(),
            observed_generation: String::new(),
            receipts: Vec::new(),
            error: String::new(),
            forceable: false,
            updated_at_ms: 0,
        }
    }

    #[test]
    fn summary_prefers_the_failure_reason() {
        let mut status = status_fixture();
        status.phase = FrameCapturePhase::Failed;
        status.error = "cluster generation drifted".to_owned();
        assert_eq!(status.summary(), "cluster generation drifted");
    }

    #[test]
    fn summary_counts_receipts_when_complete() {
        let mut status = status_fixture();
        status.phase = FrameCapturePhase::Complete;
        status.receipts = vec![
            FrameCaptureReceipt::default(),
            FrameCaptureReceipt::default(),
        ];
        assert_eq!(status.summary(), "2 artifact(s) captured");
    }
}