continuity-runtime 0.1.0

Typed continuity and incident surface crate with bounded recovery profiles; not an incident-command runtime
Documentation
#![allow(clippy::expect_used)]

use continuity_runtime::{
    ContinuityValidationError, EscalationClockPolicyV1, IncidentSeverityV1, IncidentStatusV1,
    IncidentTaxonomyV1, RecoveryReplaySliceV1, RecoveryReplayStateV1, ResilienceExerciseKindV1,
    ResilienceExerciseV1, ServiceLevelProfileV1, SeverityPostmortemClockV1,
    SeverityResponseClockV1,
};

#[test]
fn service_level_profile_requires_owner_ref() {
    let err = ServiceLevelProfileV1::new(
        "slp_001",
        "alpha-service",
        "99.9%",
        "p95<250ms",
        "",
        "ebl_001",
    )
    .expect_err("owner ref is required");
    assert_eq!(err, ContinuityValidationError::MissingField("owner_ref"));
}

#[test]
fn replay_slice_requires_gaps_when_partial() {
    let err = RecoveryReplaySliceV1::new(
        "rrs_001",
        "inc_001",
        vec!["fxr_001".to_string()],
        "2026-03-15T12:30:00Z/2026-03-15T13:00:00Z",
        RecoveryReplayStateV1::Partial,
        Vec::new(),
    )
    .expect_err("partial replay requires gaps");
    assert_eq!(err, ContinuityValidationError::InvalidState("gaps"));
}

#[test]
fn exercise_requires_follow_up_actions() {
    let err = ResilienceExerciseV1::new(
        "rex_001",
        "slp_001",
        ResilienceExerciseKindV1::Tabletop,
        "deploy regression with missing approver",
        "2026-03-10T15:00:00Z",
        Vec::new(),
    )
    .expect_err("exercise follow up actions are required");
    assert_eq!(
        err,
        ContinuityValidationError::MissingField("follow_up_actions")
    );
}

#[test]
fn escalation_policy_rejects_non_positive_clock() {
    let err = EscalationClockPolicyV1::new(
        "ecp_001",
        "sm_001",
        vec![SeverityResponseClockV1 {
            severity: IncidentSeverityV1::Sev1,
            minutes: 0,
        }],
        vec![SeverityPostmortemClockV1 {
            severity: IncidentSeverityV1::Sev1,
            hours: 72,
        }],
        vec!["pause_only_during_declared_external_outage".to_string()],
        "incident-governance-review",
    )
    .expect_err("response clock must be positive");
    assert_eq!(err, ContinuityValidationError::InvalidState("minutes"));
}

#[test]
fn taxonomy_and_status_validate_when_well_formed() {
    let taxonomy = IncidentTaxonomyV1::new(
        "itx_001",
        vec![continuity_runtime::IncidentClassRuleV1 {
            incident_class: "availability_degradation".to_string(),
            default_severity: IncidentSeverityV1::Sev2,
        }],
        vec![continuity_runtime::IncidentRouteRuleV1 {
            incident_class: "availability_degradation".to_string(),
            route: "reliability_primary".to_string(),
        }],
        vec!["IncidentCaseV1".to_string()],
    )
    .expect("taxonomy");
    taxonomy.validate().expect("taxonomy valid");

    let incident = continuity_runtime::IncidentCaseV1::new(
        "inc_001",
        "slp_001",
        IncidentSeverityV1::Sev2,
        "2026-03-15T12:30:00Z",
        "elevated latency after change window",
        "person:oncall-1",
        IncidentStatusV1::Active,
    )
    .expect("incident");
    incident.validate().expect("incident valid");
}