mobius-gateway 0.11.5

Headless authenticated gateway for möbius frontends
Documentation
use super::*;

async fn gateway(root: &tempfile::TempDir) -> GatewayHost {
    let listen = "127.0.0.1:8741".parse().expect("listen address");
    let (store, config) =
        ConfigStore::initialize(root.path().join("state"), listen, None).expect("config");
    let credentials =
        Arc::new(CredentialStore::open(store.credentials_path()).expect("credentials"));
    let bots = Arc::new(BotStore::open(store.state_dir()).expect("Bots"));
    GatewayHost::start(store, config, credentials, bots)
        .await
        .expect("gateway")
}

#[tokio::test]
async fn gateway_manages_bot_swarms_and_broadcasts_the_catalog() {
    let root = tempfile::tempdir().expect("root");
    let gateway = gateway(&root).await;
    let leader = ensure_test_bot(&gateway).await.expect("leader Bot");
    let (reviewer, tester) = {
        let state = gateway.state.lock().await;
        let config = leader.config.config.clone();
        (
            state
                .bots
                .create_bot("reviewer", "Reviewer", config.clone())
                .expect("reviewer Bot"),
            state
                .bots
                .create_bot("tester", "Tester", config)
                .expect("tester Bot"),
        )
    };
    let mut events = gateway.subscribe();

    let created = gateway
        .create_swarm(
            "Review team".into(),
            leader.id.clone(),
            vec![reviewer.id.clone()],
        )
        .await
        .expect("create swarm");
    let swarm_id = created[0].id.clone();
    assert_eq!(created[0].leader_bot_id, leader.id);
    assert_eq!(created[0].members.len(), 2);
    assert!(matches!(
        events.recv().await.expect("swarm broadcast").message,
        ServerMessage::Swarms {
            request_id: None,
            ..
        }
    ));

    let scratchpad = gateway
        .submit_scratchpad(
            &crate::wire::ScratchpadScope::Swarm {
                id: swarm_id.clone(),
            },
            Op::CapabilityCommand {
                capability: "scratchpad".into(),
                command: "scratchpad".into(),
                arguments: "add".into(),
                input: Some("Release target is Friday".into()),
                target: None,
            },
        )
        .await
        .expect("add swarm scratchpad note");
    assert!(matches!(
        &scratchpad.widgets[0].content,
        Some(mobius::protocol::FrontendWidgetContent::ActionList { items, .. })
            if items[0].text == "Release target is Friday"
    ));

    let renamed = gateway
        .rename_swarm(&swarm_id, "Release team".into())
        .await
        .expect("rename swarm");
    assert_eq!(renamed[0].title, "Release team");

    let joined = gateway
        .add_swarm_member(&swarm_id, tester.id.clone())
        .await
        .expect("add member");
    assert_eq!(joined[0].members.len(), 3);

    let left = gateway
        .leave_swarm(&swarm_id, &tester.id)
        .await
        .expect("leave swarm");
    assert_eq!(left[0].members.len(), 2);

    assert!(
        gateway
            .disband_swarm(&swarm_id)
            .await
            .expect("disband")
            .is_empty()
    );
    let scratchpad = gateway.state.lock().await.scratchpad.clone();
    let cleared = scratchpad
        .swarm_contribution(&swarm_id)
        .await
        .expect("cleared swarm scratchpad");
    assert!(matches!(
        &cleared.widgets[0].content,
        Some(mobius::protocol::FrontendWidgetContent::ActionList { items, .. }) if items.is_empty()
    ));
}

#[tokio::test]
async fn gateway_rejects_unknown_or_already_grouped_bots() {
    let root = tempfile::tempdir().expect("root");
    let gateway = gateway(&root).await;
    let leader = ensure_test_bot(&gateway).await.expect("leader Bot");
    let reviewer = {
        let state = gateway.state.lock().await;
        state
            .bots
            .create_bot("reviewer", "Reviewer", leader.config.config.clone())
            .expect("reviewer Bot")
    };
    gateway
        .create_swarm("First".into(), leader.id.clone(), vec![reviewer.id.clone()])
        .await
        .expect("first swarm");

    let unknown = gateway
        .create_swarm(
            "Unknown".into(),
            leader.id.clone(),
            vec![Uuid::new_v4().to_string()],
        )
        .await
        .expect_err("unknown Bot");
    assert_eq!(unknown.code, "invalid_bot");

    let grouped = gateway
        .create_swarm("Second".into(), leader.id.clone(), vec![reviewer.id])
        .await
        .expect_err("Bots may belong to one swarm");
    assert_eq!(grouped.code, "invalid_swarm");
}

#[tokio::test]
async fn gateway_leave_releases_host_state_while_waiting_for_delivery_acceptance() {
    let root = tempfile::tempdir().expect("root");
    let gateway = gateway(&root).await;
    let leader = ensure_test_bot(&gateway).await.expect("leader Bot");
    let reviewer = {
        let state = gateway.state.lock().await;
        state
            .bots
            .create_bot("reviewer", "Reviewer", leader.config.config.clone())
            .expect("reviewer Bot")
    };
    let swarm_id = gateway
        .create_swarm(
            "Review team".into(),
            leader.id.clone(),
            vec![reviewer.id.clone()],
        )
        .await
        .expect("create swarm")[0]
        .id
        .clone();
    let swarm = Arc::clone(&gateway.state.lock().await.swarm);
    swarm
        .post(
            &leader.id,
            &Uuid::new_v4().to_string(),
            format!("@{} please review", reviewer.handle),
            None,
        )
        .await
        .expect("post");
    let claim = swarm
        .claim_next_delivery(&reviewer.id)
        .await
        .expect("claim delivery")
        .expect("pending delivery");
    let leaving = gateway.leave_swarm(&swarm_id, &reviewer.id);
    tokio::pin!(leaving);
    tokio::select! {
        biased;
        result = &mut leaving => panic!("leave settled before queue acceptance: {result:?}"),
        () = std::future::ready(()) => {}
    }
    tokio::select! {
        biased;
        state = gateway.state.lock() => drop(state),
        () = std::future::ready(()) => {
            panic!("leave held gateway state while waiting for delivery acceptance");
        }
    }

    claim
        .accept(std::future::ready(()))
        .await
        .expect("accept delivery")
        .expect("eligible delivery");
    leaving.await.expect("leave after acceptance");
}