bmux_pane_runtime_plugin_api 0.0.1-alpha.3

Typed public API of the bmux pane-runtime plugin (BPDL-generated bindings)
//! Typed public API of the bmux pane-runtime plugin.
//!
//! This crate is the stable contract core + other plugins depend on
//! for the pane-runtime domain. Five modules are generated from
//! `bpdl/pane-runtime-plugin.bpdl` at compile time via the
//! [`bmux_plugin_schema_macros::schema!`] macro:
//! - [`pane_runtime_state`] — queries over pane/session runtime.
//! - [`pane_runtime_commands`] — mutating pane + session-runtime commands.
//! - [`attach_runtime_commands`] — per-client attach lifecycle.
//! - [`attach_runtime_state`] — attach-view queries.
//! - [`pane_runtime_events`] — lifecycle event stream.

#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
// BPDL-generated service methods accept one argument per record
// field, which trips `too_many_arguments` on rich commands like
// `launch-pane` (8 args: session_id, target, direction, ratio_percent,
// name, program, args, cwd). The macro-generated code cannot be
// refactored; allow at the crate level.
#![allow(clippy::too_many_arguments)]

pub mod input_capture;

bmux_plugin_schema_macros::schema! {
    source: "bpdl/pane-runtime-plugin.bpdl",
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PaneRuntimePluginConfig {
    pub shell: String,
    pub pane_term: String,
    #[serde(default = "default_true")]
    pub bracketed_paste: bool,
    #[serde(default)]
    pub shell_integration_root: Option<std::path::PathBuf>,
}

const fn default_true() -> bool {
    true
}

#[cfg(test)]
mod tests {
    use super::{pane_runtime_commands, pane_runtime_state};

    #[test]
    fn program_execution_contract_keeps_literal_binary_input_and_identity() {
        use super::pane_runtime_execution_v4::{TerminalExecutionRef, client::SendRequest};
        let execution = TerminalExecutionRef {
            session_id: uuid::Uuid::new_v4(),
            pane_id: uuid::Uuid::new_v4(),
            execution_token: uuid::Uuid::new_v4(),
        };
        let request = SendRequest {
            execution: execution.clone(),
            data: vec![0, 255, 10],
        };
        let encoded = bmux_codec::to_vec(&request).unwrap();
        let decoded: SendRequest = bmux_codec::from_bytes(&encoded).unwrap();
        assert_eq!(decoded.execution, execution);
        assert_eq!(decoded.data, request.data);
    }

    #[test]
    fn fresh_shell_directory_is_an_explicit_versioned_contract() {
        use super::pane_runtime_spawn_commands_v1::client::NewSessionRequest;
        use super::pane_runtime_spawn_v1::InitialShellOptions;

        for cwd in [None, Some("/work/project".to_string())] {
            let request = NewSessionRequest {
                name: Some("work".to_string()),
                options: InitialShellOptions { cwd: cwd.clone() },
            };
            let encoded = bmux_codec::to_vec(&request).expect("encode spawn request");
            let decoded: NewSessionRequest =
                bmux_codec::from_bytes(&encoded).expect("decode spawn request");
            assert_eq!(decoded.options.cwd, cwd);
            assert_eq!(decoded.name, request.name);
        }
    }

    #[test]
    fn source_capture_requires_explicit_pane_identity() {
        use super::pane_runtime_spawn_v1::client::CaptureSourceRequest;
        #[derive(serde::Serialize)]
        struct MissingPane {
            session_id: uuid::Uuid,
        }
        let request = CaptureSourceRequest {
            session_id: uuid::Uuid::new_v4(),
            pane_id: uuid::Uuid::new_v4(),
        };
        let bytes = bmux_codec::to_vec(&request).unwrap();
        let decoded: CaptureSourceRequest = bmux_codec::from_bytes(&bytes).unwrap();
        assert_eq!(decoded.session_id, request.session_id);
        assert_eq!(decoded.pane_id, request.pane_id);
        // There is deliberately no optional/global Active selector on this route.
        let missing = bmux_codec::to_vec(&MissingPane {
            session_id: request.session_id,
        })
        .unwrap();
        assert!(bmux_codec::from_bytes::<CaptureSourceRequest>(&missing).is_err());
    }

    #[test]
    fn directory_observation_preserves_execution_identity_and_provenance() {
        use super::pane_runtime_spawn_v1::{
            DirectoryObservation, ObservationMethod, SourceExecution,
        };
        let observation = DirectoryObservation {
            source: SourceExecution {
                session_id: uuid::Uuid::new_v4(),
                pane_id: uuid::Uuid::new_v4(),
                execution_token: uuid::Uuid::new_v4(),
            },
            path: "/work/project".to_string(),
            method: ObservationMethod::Shell,
        };
        let encoded = bmux_codec::to_vec(&observation).expect("encode observation");
        let decoded: DirectoryObservation =
            bmux_codec::from_bytes(&encoded).expect("decode observation");
        assert_eq!(decoded, observation);
    }

    #[test]
    fn versioned_capture_preserves_legacy_pin_wire_shape() {
        use super::attach_runtime_state::{HistoryCaptureV1, PaneScrollbackPin};
        #[derive(serde::Serialize)]
        struct LegacyPin {
            pane_id: uuid::Uuid,
            pin_id: u64,
            total_scrolled_rows: u64,
            max_scrollback_offset: u32,
            stream_end: u64,
        }
        let pin = PaneScrollbackPin {
            pane_id: uuid::Uuid::new_v4(),
            pin_id: 7,
            total_scrolled_rows: 12,
            max_scrollback_offset: 3,
            stream_end: 99,
        };
        let legacy = LegacyPin {
            pane_id: pin.pane_id,
            pin_id: pin.pin_id,
            total_scrolled_rows: pin.total_scrolled_rows,
            max_scrollback_offset: pin.max_scrollback_offset,
            stream_end: pin.stream_end,
        };
        assert_eq!(
            bmux_codec::to_vec(&pin).unwrap(),
            bmux_codec::to_vec(&legacy).unwrap()
        );
        let capture = HistoryCaptureV1 {
            width: 80,
            height: 24,
            history_truncated: true,
            history_line_count: 3,
            capture_id: uuid::Uuid::new_v4(),
            pin,
        };
        let bytes = bmux_codec::to_vec(&capture).unwrap();
        let decoded: HistoryCaptureV1 = bmux_codec::from_bytes(&bytes).unwrap();
        assert_eq!(decoded, capture);
    }

    use pane_runtime_commands::client::SetPanePaddingRequest;

    #[test]
    fn pane_padding_generated_models_preserve_complete_contract_values() {
        let spec = pane_runtime_state::PanePaddingSpec {
            left: 1,
            right: 2,
            top: 3,
            bottom: 4,
            max_content_width: Some(120),
            max_content_height: Some(40),
            horizontal_alignment: "center".to_string(),
            vertical_alignment: "bottom".to_string(),
        };
        let state = pane_runtime_state::PanePaddingState {
            session_id: uuid::Uuid::new_v4(),
            pane_id: uuid::Uuid::new_v4(),
            declarative: spec.clone(),
            matched_rule_index: Some(2),
            runtime_override: Some(spec.clone()),
            effective: spec.clone(),
            source: "runtime_override".to_string(),
            outer_rect: Some(pane_runtime_state::PanePaddingRect {
                x: 0,
                y: 0,
                w: 160,
                h: 50,
            }),
            base_content_rect: None,
            effective_content_rect: Some(pane_runtime_state::PanePaddingRect {
                x: 20,
                y: 5,
                w: 120,
                h: 40,
            }),
            persist_runtime_overrides: true,
        };

        let encoded = bmux_codec::to_vec(&state).expect("encode generated padding state");
        let decoded: pane_runtime_state::PanePaddingState =
            bmux_codec::from_bytes(&encoded).expect("decode generated padding state");
        assert_eq!(decoded, state);

        let request = SetPanePaddingRequest {
            session_id: state.session_id,
            pane_id: Some(state.pane_id),
            padding: spec,
        };
        let encoded = bmux_codec::to_vec(&request).expect("encode generated set request");
        let decoded: SetPanePaddingRequest =
            bmux_codec::from_bytes(&encoded).expect("decode generated set request");
        assert_eq!(decoded.session_id, request.session_id);
        assert_eq!(decoded.pane_id, request.pane_id);
        assert_eq!(decoded.padding, request.padding);
    }
}