cellos-server 0.5.4

HTTP control plane for CellOS — admission, projection over JetStream, WebSocket fan-out of CloudEvents. Pure event-sourced architecture.
Documentation
//! Wave 2 red-team — boundary contract test (CRIT-W2D-1, HIGH-W2D-2).
//!
//! Wave 1 found that `cellos-server::state::apply_event_payload`'s
//! event-type discriminator was hard-coded to the legacy
//! `io.cellos.formation.v1.*` prefix while `cellos-core::events` emits the
//! canonical `dev.cellos.events.cell.formation.v1.*` family — every
//! formation replayed from JetStream silently fell through the gate and
//! the server's in-memory `status` projection stayed frozen at PENDING
//! forever. Wave-1 reports flagged this as cross-crate and deferred it;
//! wave-2 fixes it and pins a contract so the same drift cannot return.
//!
//! This test walks the canonical CloudEvent constructors in
//! `cellos-core::events` and asserts that for each `formation.v1.<phase>`
//! emitter, `apply_event_payload` produces the expected
//! [`FormationStatus`]. It is the e2e counterpart to FC-74's lint
//! (`crates/cellos-supervisor/tests/fc74_audit_retention_no_drop.rs`)
//! and the reducer-coverage test in `web/packages/cellos-events`.

use cellos_core::events::{
    cloud_event_v1_formation_completed, cloud_event_v1_formation_created,
    cloud_event_v1_formation_degraded, cloud_event_v1_formation_failed,
    cloud_event_v1_formation_launching, cloud_event_v1_formation_running,
};
use cellos_server::{AppState, FormationStatus};
use uuid::Uuid;

/// Render the canonical CloudEvent emitter's output as a JSON byte
/// buffer suitable for `apply_event_payload`.
fn ce_bytes(ev: cellos_core::CloudEventV1) -> Vec<u8> {
    serde_json::to_vec(&ev).expect("CloudEventV1 serialises")
}

/// Tuple of (phase tag, CloudEvent constructor, expected projected
/// status). Factored out to keep the case table below readable and
/// silence clippy::type_complexity on the inline form.
type PhaseCase = (
    &'static str,
    &'static dyn Fn(&str, &str) -> cellos_core::CloudEventV1,
    FormationStatus,
);

/// CRIT-W2D-1: every canonical formation-phase emitter must drive the
/// server's projection to the correct `FormationStatus`. Before the
/// wave-2 fix, every emitter hit the legacy-prefix gate and was ignored;
/// `status` stayed PENDING regardless of the canonical phase.
#[tokio::test]
async fn canonical_formation_emitters_drive_projection_to_expected_status() {
    let cases: &[PhaseCase] = &[
        (
            "created",
            &|id, name| {
                cloud_event_v1_formation_created(id, "1970-01-01T00:00:00Z", id, name, 0, &[], None)
            },
            FormationStatus::Pending,
        ),
        (
            "launching",
            &|id, name| {
                cloud_event_v1_formation_launching(
                    id,
                    "1970-01-01T00:00:00Z",
                    id,
                    name,
                    0,
                    &[],
                    None,
                )
            },
            FormationStatus::Running,
        ),
        (
            "running",
            &|id, name| {
                cloud_event_v1_formation_running(id, "1970-01-01T00:00:00Z", id, name, 0, &[], None)
            },
            FormationStatus::Running,
        ),
        (
            "degraded",
            &|id, name| {
                cloud_event_v1_formation_degraded(
                    id,
                    "1970-01-01T00:00:00Z",
                    id,
                    name,
                    1,
                    &["cell-a".to_string()],
                    Some("critical cell down"),
                )
            },
            FormationStatus::Running,
        ),
        (
            "completed",
            &|id, name| {
                cloud_event_v1_formation_completed(
                    id,
                    "1970-01-01T00:00:00Z",
                    id,
                    name,
                    0,
                    &[],
                    None,
                )
            },
            FormationStatus::Succeeded,
        ),
        (
            "failed",
            &|id, name| {
                cloud_event_v1_formation_failed(
                    id,
                    "1970-01-01T00:00:00Z",
                    id,
                    name,
                    1,
                    &["cell-a".to_string()],
                    Some("admission gate"),
                )
            },
            FormationStatus::Failed,
        ),
    ];

    let state = AppState::new(None, "test");

    for (phase, build, expected) in cases {
        let id = Uuid::new_v4();
        let id_str = id.to_string();
        let name = format!("wave2-{phase}-formation");
        let ev = (build)(id_str.as_str(), name.as_str());
        let payload = ce_bytes(ev);

        let outcome = state
            .apply_event_payload(&payload)
            .await
            .unwrap_or_else(|e| panic!("apply_event_payload failed for phase {phase}: {e}"));

        assert!(
            matches!(outcome, cellos_server::state::ApplyOutcome::Applied),
            "phase {phase}: expected ApplyOutcome::Applied, got {outcome:?}"
        );

        let map = state.formations.read().await;
        let rec = map.get(&id).unwrap_or_else(|| {
            panic!("phase {phase}: formation {id_str} not present in projection")
        });
        assert_eq!(
            rec.status, *expected,
            "phase {phase}: status drift — got {:?}, want {:?}",
            rec.status, expected
        );
        assert_eq!(
            rec.name, name,
            "phase {phase}: name drift — got {:?}, want {:?}",
            rec.name, name
        );
        drop(map);
    }
}

/// Legacy `io.cellos.formation.v1.*` events still replay cleanly — the
/// fix accepts BOTH prefixes so older audit archives don't go dark.
/// snake_case `formation_id` is still honoured on the legacy shape.
#[tokio::test]
async fn legacy_io_cellos_formation_prefix_still_replays() {
    let state = AppState::new(None, "test");
    let id = Uuid::new_v4();
    let payload = serde_json::to_vec(&serde_json::json!({
        "specversion": "1.0",
        "id": "legacy-evt-1",
        "source": format!("/formations/{id}"),
        "type": "io.cellos.formation.v1.created",
        "data": {
            "formation_id": id.to_string(),
            "name": "legacy-shape",
        },
    }))
    .unwrap();

    let outcome = state
        .apply_event_payload(&payload)
        .await
        .expect("legacy shape must apply");
    assert!(matches!(
        outcome,
        cellos_server::state::ApplyOutcome::Applied
    ));
    let map = state.formations.read().await;
    let rec = map.get(&id).expect("legacy formation present");
    assert_eq!(rec.status, FormationStatus::Pending);
    assert_eq!(rec.name, "legacy-shape");
}

/// HIGH-W2D-2 wire contract: the JSON-serialised
/// [`cellos_server::state::FormationRecord`] uses field name `status`
/// (UPPERCASE values). `cellctl`'s [`cellos_ctl::model::Formation`]
/// expects field `state` and accepts `status` via `serde(alias)`. Pin
/// the bytes here so a future rename in either crate has to land
/// together.
#[test]
fn formation_record_serialises_status_field_in_uppercase() {
    let rec = cellos_server::FormationRecord {
        id: Uuid::nil(),
        name: "ctr".into(),
        status: FormationStatus::Running,
        document: serde_json::Value::Null,
    };
    let body = serde_json::to_value(&rec).expect("serialise");
    assert!(
        body.get("status").is_some(),
        "FormationRecord wire shape must keep `status` (cellos-events depends on it): {body}"
    );
    assert_eq!(
        body.get("status").and_then(|v| v.as_str()),
        Some("RUNNING"),
        "FormationStatus must serialise UPPERCASE: {body}"
    );
}

// HIGH-W2D-2 client side: cellctl's `Formation` model accepts `status` via
// serde alias — see `crates/cellos-ctl/tests/formation_wire_alias.rs` for
// the matching round-trip pin. We deliberately don't pull `cellos-ctl` in
// as a dev-dep of `cellos-server` to avoid an unnecessary edge in the
// crate dep graph.