khive-gate 0.14.0

Pluggable authorization gate trait + default AllowAllGate impl for khive verb dispatch.
Documentation
#[test]
fn actor_ref_anonymous() {
    let a = ActorRef::anonymous();
    assert_eq!(a.kind, "anonymous");
    assert_eq!(a.id, "local");
}

#[test]
fn anonymous_actor_binding_id_is_none() {
    let a = ActorRef::anonymous();
    assert!(a.is_anonymous());
    assert_eq!(
        a.binding_id(),
        None,
        "anonymous actor must never expose a binding_id, even though its \
         raw id (\"local\") could otherwise match an explicit actor=\"local\" binding"
    );
}

#[test]
fn configured_actor_binding_id_is_its_id() {
    let a = ActorRef::new("lambda", "leo");
    assert!(!a.is_anonymous());
    assert_eq!(a.binding_id(), Some("leo"));
}

#[test]
fn actor_label_collapses_only_the_plain_actor_kind() {
    assert_eq!(ActorRef::new("actor", "svc:build").label(), "svc:build");
    assert_eq!(ActorRef::new("agent", "x").label(), "agent:x");
    assert_eq!(
        ActorRef::new("anonymous", "local").label(),
        "anonymous:local"
    );
}

#[test]
fn split_stamped_label_splits_only_runtime_stamped_kinds() {
    assert_eq!(
        split_stamped_label("actor:svc:build"),
        Some(("actor", "svc:build"))
    );
    assert_eq!(split_stamped_label("agent:x"), Some(("agent", "x")));
    assert_eq!(split_stamped_label("svc:build"), None);
    assert_eq!(split_stamped_label("plain"), None);
}

#[test]
fn decision_helpers() {
    assert!(GateDecision::allow().is_allow());
    assert!(!GateDecision::deny("nope").is_allow());
}

#[test]
fn request_serializes_to_stable_shape() {
    let req = sample_request();
    let v = serde_json::to_value(&req).unwrap();
    assert_eq!(v["actor"]["kind"], "anonymous");
    assert_eq!(v["actor"]["id"], "local");
    assert_eq!(v["namespace"], "local");
    assert_eq!(v["verb"], "search");
    assert_eq!(v["args"]["query"], "LoRA");
}

#[test]
fn decision_roundtrips_through_json() {
    let allow = GateDecision::allow_with(vec![Obligation::Audit {
        tag: "search.attempt".into(),
    }]);
    let s = serde_json::to_string(&allow).unwrap();
    let back: GateDecision = serde_json::from_str(&s).unwrap();
    match back {
        GateDecision::Allow { obligations } => {
            assert_eq!(obligations.len(), 1);
            match &obligations[0] {
                Obligation::Audit { tag } => assert_eq!(tag, "search.attempt"),
                _ => panic!("expected Audit"),
            }
        }
        _ => panic!("expected Allow"),
    }

    let deny = GateDecision::deny("forbidden");
    let s = serde_json::to_string(&deny).unwrap();
    let back: GateDecision = serde_json::from_str(&s).unwrap();
    match back {
        GateDecision::Deny { reason } => assert_eq!(reason, "forbidden"),
        _ => panic!("expected Deny"),
    }
}

#[test]
fn obligation_rate_limit_serializes_with_kind_tag() {
    let o = Obligation::RateLimit {
        window_secs: 60,
        max: 100,
    };
    let v = serde_json::to_value(&o).unwrap();
    assert_eq!(v["kind"], "rate_limit");
    assert_eq!(v["window_secs"], 60);
    assert_eq!(v["max"], 100);
}