phoxal 0.66.0

Phoxal - production-oriented autonomous robot framework: the one framework library, holding the runtime engine, the api contract tree, the typed bus, the canonical model, and the bundle.
Documentation
//! The frozen attachment bootstrap: the one exchange two Phoxal binaries can
//! complete before they know whether they agree on anything else.
//!
//! `supervisor/connect` answers exactly one question - which framework train
//! built this supervisor - so a client that disagrees can say so precisely
//! instead of failing to decode a richer reply and reporting nothing.
//!
//! The reply reports the exact version, never a line or a verdict. Deciding
//! compatibility is the reader's, through
//! [`FrameworkVersion::is_compatible_with`]; a supervisor that shipped a
//! verdict would be answering a question only the peer can ask.
//!
//! # The promise includes the transport beneath it
//!
//! A frozen document is worth nothing if the path to it moves: a client that
//! cannot reach this key never gets to decode the one reply that would have
//! named the disagreement. So the freeze covers everything an attaching client
//! traverses before this reply, each owned and pinned by [`crate::bus`]:
//!
//! - **discovery** - a client-mode session on the operator's endpoint, with
//!   multicast scouting off, reading the executions of the routers it is
//!   directly connected to;
//! - **the key grammar** - `phoxal/{execution}/{topic}`, which this endpoint's
//!   own `supervisor/connect` composes into;
//! - **the query envelopes** - the `BusMetadata` attachment a reply carries and
//!   the `QueryFailure` body the error leg carries;
//! - **the encoding** - `phoxal/v0;codec=1`, MessagePack with named fields;
//! - **the Zenoh wire protocol version**, without which no session forms at
//!   all.
//!
//! All of them are preserved across framework majors on the same terms as the
//! documents below.

crate::endpoints! {
    self: Query<ConnectRequest, ConnectReply>;
}

use crate::version::FrameworkVersion;

/// Execution-scoped supervisor presence lease. This is a Liveliness key, not
/// an endpoint payload, and therefore deliberately sits outside the endpoint
/// manifest. It composes under one already-known execution root and signals
/// loss of that execution's control-plane authority; it performs no discovery.
///
/// It lives beside the bootstrap because the two are one attachment surface: a
/// client learns the train here and watches this key to learn the authority is
/// gone.
pub const PRESENCE_KEY: &str = "supervisor/presence";

/// The bootstrap request.
///
/// `V0` is frozen for every future framework line. It carries no fields and
/// never will: any argument would be a second thing the two peers must already
/// agree on before they are allowed to disagree.
#[derive(
    phoxal_macros::DescribeWire,
    Clone,
    Copy,
    Debug,
    Eq,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
)]
#[serde(tag = "schema")]
pub enum ConnectRequest {
    #[serde(rename = "phoxal/supervisor-connect/v0")]
    V0 {},
}

/// The bootstrap reply: the framework train that built this supervisor, and
/// nothing else.
///
/// `V0` is frozen for every future framework line. It never grows a field -
/// no process state, robot identity, clock mode, manifest data, capability
/// list, CLI version, schema inventory, or node topology - so a binary from any
/// line can decode it and name the mismatch. Everything else a client wants is
/// behind an ordinary endpoint that the client may only call once the trains
/// agree.
#[derive(
    phoxal_macros::DescribeWire,
    Clone,
    Copy,
    Debug,
    Eq,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
)]
#[serde(tag = "schema")]
pub enum ConnectReply {
    #[serde(rename = "phoxal/supervisor-connect/v0")]
    V0 { framework: FrameworkVersion },
}


#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::{ConnectReply, ConnectRequest, FrameworkVersion, PRESENCE_KEY};

    /// The key the tree renders for the bootstrap, against the literal it is
    /// frozen at. The literal is written out rather than read back from the
    /// tree: a frozen bootstrap that inherits its key from whatever the tree
    /// currently renders is not frozen at all.
    #[test]
    fn the_bootstrap_key_is_pinned_to_its_literal() {
        let bootstrap = crate::supervisor::api::topics().connect().client();
        assert_eq!(bootstrap.key(), "supervisor/connect");
        assert_ne!(PRESENCE_KEY, bootstrap.key());
    }

    /// The key a client actually addresses is the bus root composed with this
    /// topic, so the composed spelling is pinned as a literal too: the freeze
    /// covers the whole path to the bootstrap, not only its last segment. The
    /// grammar is read out of the bus's own declared surface rather than
    /// restated here, so a crate that moved it cannot leave this test agreeing
    /// with itself.
    ///
    /// That the live session composes the same key is proved separately, end to
    /// end, by `phoxal`'s `wire_key_composition` test. That one composes from
    /// whatever the two halves currently are, which is what makes this literal
    /// the pin rather than a second copy of it.
    #[test]
    fn the_composed_bootstrap_wire_key_is_pinned_to_its_literal() {
        const EXECUTION: &str = "1c8f3a5b7d9e0f2a4b6c8d0e1f325476";

        let surface = crate::bus::__compat::contract_surface();
        assert!(
            surface.contains(
                r#"{"name":"bus-key-composition","record":"identifier","value":"phoxal/{execution}/{topic}"}"#
            ),
            "the bus must declare the key grammar the bootstrap composes into: {surface}"
        );

        let composed = "phoxal/{execution}/{topic}"
            .replace("{execution}", EXECUTION)
            .replace(
                "{topic}",
                crate::supervisor::api::topics().connect().client().key(),
            );
        assert_eq!(
            composed,
            "phoxal/1c8f3a5b7d9e0f2a4b6c8d0e1f325476/supervisor/connect"
        );
    }

    /// Both documents are pinned as literal JSON, including the tag strings and
    /// the canonical framework spelling, so no serde attribute change can move
    /// them without this test saying so.
    #[test]
    fn the_bootstrap_documents_are_pinned_to_their_literal_json() {
        assert_eq!(
            serde_json::to_value(ConnectRequest::V0 {}).unwrap(),
            json!({"schema": "phoxal/supervisor-connect/v0"})
        );
        assert_eq!(
            serde_json::to_value(ConnectReply::V0 {
                framework: FrameworkVersion::CURRENT,
            })
            .unwrap(),
            json!({
                "schema": "phoxal/supervisor-connect/v0",
                "framework": FrameworkVersion::CURRENT_SPELLING,
            })
        );
        // A fixed train, so the spelling itself is pinned and not merely
        // whatever `CURRENT` happens to render to today.
        assert_eq!(
            serde_json::to_value(ConnectReply::V0 {
                framework: FrameworkVersion::new(9, 9, 9),
            })
            .unwrap(),
            json!({
                "schema": "phoxal/supervisor-connect/v0",
                "framework": "9.9.9",
            })
        );
        assert_eq!(
            serde_json::from_value::<ConnectReply>(json!({
                "schema": "phoxal/supervisor-connect/v0",
                "framework": "9.9.9",
            }))
            .unwrap(),
            ConnectReply::V0 {
                framework: FrameworkVersion::new(9, 9, 9),
            }
        );
    }

    /// The frozen documents are pinned as a wire shape too, not only as
    /// literal JSON: the canonical rendering is what compatibility CI compares
    /// against a published baseline, so a change to it has to be deliberate.
    /// The reply also proves the derive composes with a hand-written
    /// declaration from another crate - the framework version is one string,
    /// never the three-field struct it is made of.
    #[test]
    fn the_bootstrap_documents_are_pinned_to_their_declared_wire_shape() {
        use crate::__compat::wire::DescribeWire;

        assert_eq!(
            ConnectRequest::wire_schema().canonical_json(),
            concat!(
                r#"{"kind":"enum","representation":{"style":"internal","tag":"schema"},"#,
                r#""variants":[{"body":{"fields":[],"kind":"struct"},"#,
                r#""name":"phoxal/supervisor-connect/v0"}]}"#,
            )
        );
        assert_eq!(
            ConnectReply::wire_schema().canonical_json(),
            concat!(
                r#"{"kind":"enum","representation":{"style":"internal","tag":"schema"},"#,
                r#""variants":[{"body":{"fields":[{"name":"framework","presence":"required",""#,
                r#"schema":{"kind":"opaque","name":"FrameworkVersion","wire":{"kind":"string"}}}],"#,
                r#""kind":"struct"},"name":"phoxal/supervisor-connect/v0"}]}"#,
            )
        );

        for document in [
            serde_json::to_value(ConnectRequest::V0 {}).expect("the request serializes"),
            serde_json::to_value(ConnectReply::V0 {
                framework: FrameworkVersion::CURRENT,
            })
            .expect("the reply serializes"),
        ] {
            let schema = if document.get("framework").is_some() {
                ConnectReply::wire_schema()
            } else {
                ConnectRequest::wire_schema()
            };
            assert_eq!(schema.conforms(&document), Ok(()), "{document}");
        }
    }

    /// The bus codec is MessagePack, so the freeze has to hold there too.
    ///
    /// The reply is round-tripped at this train and at a released major: the
    /// bootstrap is what two binaries exchange before they know whether their
    /// trains agree, so it has to survive the flip to a Stable line as
    /// literally as it survives a patch.
    #[test]
    fn the_bootstrap_documents_round_trip_on_the_bus_codec() {
        let request = ConnectRequest::V0 {};
        let encoded = rmp_serde::to_vec_named(&request).unwrap();
        assert_eq!(
            rmp_serde::from_slice::<ConnectRequest>(&encoded).unwrap(),
            request
        );

        for framework in [FrameworkVersion::CURRENT, FrameworkVersion::new(9, 9, 9)] {
            let reply = ConnectReply::V0 { framework };
            let encoded = rmp_serde::to_vec_named(&reply).unwrap();
            assert_eq!(
                rmp_serde::from_slice::<ConnectReply>(&encoded).unwrap(),
                reply
            );
        }
    }

    /// A reply from a line this binary does not implement fails by naming the
    /// foreign tag, which is the whole point of tagging the bootstrap.
    #[test]
    fn a_foreign_schema_tag_fails_by_naming_itself() {
        const FOREIGN: &str = "phoxal/supervisor-connect/v1";
        let foreign = json!({"schema": FOREIGN, "framework": "9.9.9"});

        let error = serde_json::from_value::<ConnectReply>(foreign.clone()).unwrap_err();
        assert!(
            error.to_string().contains(FOREIGN),
            "the mismatch diagnostic must name the foreign tag: {error}"
        );

        let encoded = rmp_serde::to_vec_named(&foreign).unwrap();
        let error = rmp_serde::from_slice::<ConnectReply>(&encoded).unwrap_err();
        assert!(
            error.to_string().contains(FOREIGN),
            "the mismatch diagnostic must name the foreign tag: {error}"
        );
    }
}