gsm-core 0.4.45

Core types and platform abstractions for the Greentic messaging runtime.
Documentation
use gsm_core::*;
use std::sync::{Mutex, OnceLock};

fn env_lock() -> &'static Mutex<()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
}

#[test]
fn envelope_validates() {
    let env = MessageEnvelope {
        tenant: "acme".into(),
        platform: Platform::Telegram,
        chat_id: "room-1".into(),
        user_id: "u-9".into(),
        thread_id: None,
        msg_id: "msg-123".into(),
        text: Some("hi".into()),
        timestamp: "2025-10-14T09:00:00Z".into(),
        context: Default::default(),
    };
    assert!(validate_envelope(&env).is_ok());
}

#[test]
fn envelope_invalid_timestamp() {
    let mut env = MessageEnvelope {
        tenant: "acme".into(),
        platform: Platform::Slack,
        chat_id: "room-1".into(),
        user_id: "u-9".into(),
        thread_id: None,
        msg_id: "msg-123".into(),
        text: Some("hi".into()),
        timestamp: "bad-time".into(),
        context: Default::default(),
    };
    assert!(validate_envelope(&env).is_err());
    env.timestamp = "2025-10-14T10:00:00Z".into();
    assert!(validate_envelope(&env).is_ok());
}

#[test]
fn subjects_helpers_ok() {
    assert_eq!(
        ingress_subject("dev", "acme", "team", "slack"),
        "greentic.messaging.ingress.dev.acme.team.slack"
    );
    assert_eq!(
        egress_subject("dev", "acme", "team a", "web chat"),
        "greentic.messaging.egress.dev.acme.team-a.web-chat"
    );
}

#[test]
fn out_text_and_card_validate() {
    let ctx = make_tenant_ctx("acme".into(), None, None);
    let mut out = OutMessage {
        ctx: ctx.clone(),
        tenant: "acme".into(),
        platform: Platform::Teams,
        chat_id: "chat-1".into(),
        thread_id: None,
        kind: OutKind::Text,
        text: Some("hello".into()),
        message_card: None,
        #[cfg(feature = "adaptive-cards")]
        adaptive_card: None,
        meta: Default::default(),
    };
    assert!(validate_out(&out).is_ok());

    out.kind = OutKind::Card;
    out.text = None;
    out.message_card = Some(MessageCard {
        title: Some("Title".into()),
        body: vec![CardBlock::Text {
            text: "Body".into(),
            markdown: true,
        }],
        actions: vec![],
    });
    assert!(validate_out(&out).is_ok());
}

#[test]
fn current_env_defaults_to_dev() {
    let _guard = env_lock().lock().unwrap();
    let prev = current_env();
    set_current_env(EnvId::try_from("dev").expect("valid env id"));
    let env = current_env();
    assert_eq!(env.as_str(), "dev");
    set_current_env(prev);
}

#[test]
fn messaging_credentials_path_includes_env() {
    let _guard = env_lock().lock().unwrap();
    let prev = current_env();
    set_current_env(EnvId::try_from("test").expect("valid env id"));

    let ctx = make_tenant_ctx("acme".into(), Some("team-1".into()), Some("user-9".into()));
    let secret = messaging_credentials("telegram", &ctx);
    let rendered = secret.to_uri();
    assert!(
        rendered.contains("messaging"),
        "path missing messaging segment: {rendered}"
    );
    assert!(rendered.contains("/test/"));
    assert!(rendered.contains("/acme/"));
    assert!(rendered.contains("/team-1/"));

    set_current_env(prev);
}

#[test]
fn slack_workspace_secret_includes_scope() {
    let ctx = make_tenant_ctx("acme".into(), Some("team-1".into()), None);
    let path = slack_workspace_secret(&ctx, "T123");
    let rendered = path.to_uri();
    assert!(rendered.contains("messaging"));
    assert!(rendered.contains("/acme/"));
    assert!(rendered.contains("/team-1/"));
    assert!(rendered.contains("slack.workspace.t123.json"));
}

#[test]
fn slack_workspace_index_includes_scope() {
    let ctx = make_tenant_ctx("acme".into(), Some("team-1".into()), None);
    let path = crate::slack_workspace_index(&ctx);
    let rendered = path.to_uri();
    assert!(rendered.contains("messaging"));
    assert!(rendered.contains("/acme/"));
    assert!(rendered.contains("/team-1/"));
    assert!(rendered.contains("slack.workspace.index.json"));
}

#[test]
fn teams_conversations_secret_includes_scope() {
    let ctx = make_tenant_ctx("acme".into(), Some("support".into()), None);
    let path = crate::teams_conversations_secret(&ctx);
    let rendered = path.to_uri();
    assert!(rendered.contains("messaging"));
    assert!(rendered.contains("/acme/"));
    assert!(rendered.contains("/support/"));
    assert!(rendered.contains("teams.conversations.json"));
}

#[test]
fn webex_credentials_path_includes_scope() {
    let _guard = env_lock().lock().unwrap();
    let prev = current_env();
    set_current_env(EnvId::try_from("dev").expect("valid env id"));

    let ctx = make_tenant_ctx("acme".into(), Some("team-1".into()), None);
    let path = webex_credentials(&ctx);
    let rendered = path.uri().to_string();
    assert_eq!(
        rendered,
        "secrets://dev/acme/team-1/messaging/webex.credentials.json"
    );

    set_current_env(prev);
}

#[test]
fn whatsapp_credentials_path_includes_scope() {
    let ctx = make_tenant_ctx("acme".into(), None, None);
    let path = whatsapp_credentials(&ctx);
    let rendered = path.uri().to_string();
    assert_eq!(
        rendered,
        "secrets://dev/acme/_/messaging/whatsapp.credentials.json"
    );
}