anytype 0.5.0

An ergonomic Anytype API client in rust
Documentation
//! Ignored live chat discovery and message workflow coverage.
//!
//! Each test uses a fresh prefix-authorized disposable space and proves whether
//! its callback ran. Run serially with explicit disposable-process admission.

mod common;

use std::{
    future::Future,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    },
};

use anytype::{
    prelude::*,
    test_util::{
        DisposableRun, TestError, TestResult, unique_suffix, with_disposable_space_context,
    },
};
use common::retry_definitive_rate_limit;
use tokio::time::{Duration, timeout};

const LIVE_OPERATION_TIMEOUT: Duration = Duration::from_secs(20);

fn assert_disposable_completed(outcome: DisposableRun<()>, callback_ran: &AtomicBool, suite: &str) {
    match outcome {
        DisposableRun::Completed(()) => assert!(callback_ran.load(Ordering::SeqCst)),
        DisposableRun::Skipped(reason) => {
            assert!(!callback_ran.load(Ordering::SeqCst));
            eprintln!("{suite} skipped before callback: {reason:?}");
        }
    }
}

async fn bounded_api<T>(
    operation: &'static str,
    future: impl Future<Output = Result<T, AnytypeError>>,
) -> TestResult<T> {
    timeout(LIVE_OPERATION_TIMEOUT, future)
        .await
        .map_err(|_| TestError::Assertion {
            message: format!("{operation} exceeded its fixed live-test timeout"),
        })?
        .map_err(Into::into)
}

#[tokio::test]
#[ignore = "requires configured real server and disposable test admission"]
#[serial_test::serial(disposable_anytype_api)]
async fn test_chat_discovery_requests() {
    let callback_ran = Arc::new(AtomicBool::new(false));
    let callback_flag = callback_ran.clone();
    let outcome = Box::pin(with_disposable_space_context(
        "chat-discovery-resolver",
        move |ctx| {
            callback_flag.store(true, Ordering::SeqCst);
            Box::pin(async move {
                let name = format!("chat-discovery-{}", unique_suffix());
                let chat = retry_definitive_rate_limit("chat discovery setup chat", || async {
                    ctx.client
                        .chats()
                        .in_space(&ctx.space_id)
                        .create(
                            &name,
                            Icon::Emoji {
                                emoji: "🔎".to_string(),
                            },
                        )
                        .create()
                        .await
                })
                .await?;
                ctx.register_object(&chat.id);

                let chats = bounded_api(
                    "REST chat inventory",
                    ctx.client.chats().list_chats_in(&ctx.space_id).list(),
                )
                .await?;
                assert!(
                    chats.items.iter().any(|item| item.id == chat.id),
                    "REST chat listing should include the created chat"
                );

                let search = bounded_api(
                    "gRPC chat search",
                    ctx.client
                        .chats()
                        .search_chats_in(&ctx.space_id)
                        .text(&name)
                        .search(),
                )
                .await?;
                assert!(
                    search.items.iter().any(|item| item.id == chat.id),
                    "gRPC chat-object search should include the created chat"
                );

                let resolved = ctx
                    .client
                    .chats()
                    .resolve_chat_by_name(&ctx.space_id, &name)
                    .resolve()
                    .await?;
                assert_eq!(resolved, chat.id);

                let scoped_target = bounded_api(
                    "space-scoped public chat resolution",
                    ctx.client.resolve_chat_target(Some(&ctx.space_id), &name),
                )
                .await?;
                assert_eq!(scoped_target.chat_id, chat.id);
                assert_eq!(
                    scoped_target.space_id.as_deref(),
                    Some(ctx.space_id.as_str())
                );

                let direct_target = bounded_api(
                    "bare public chat-id resolution",
                    ctx.client.resolve_chat_target(None, &chat.id),
                )
                .await?;
                assert_eq!(direct_target.chat_id, chat.id);
                assert_eq!(direct_target.space_id, None);

                let fetched = ctx
                    .client
                    .chats()
                    .get_chat(&ctx.space_id, &chat.id)
                    .get()
                    .await?;
                assert_eq!(fetched.id, chat.id);
                Ok(())
            })
        },
    ))
    .await
    .expect("cleanup-safe chat-discovery live harness");
    assert_disposable_completed(outcome, &callback_ran, "chat-discovery live suite");
}

#[tokio::test]
#[ignore = "requires configured real server and disposable test admission"]
#[serial_test::serial(disposable_anytype_api)]
async fn test_chat_message_resolution_requests() {
    let callback_ran = Arc::new(AtomicBool::new(false));
    let callback_flag = callback_ran.clone();
    let outcome = Box::pin(with_disposable_space_context(
        "chat-discovery-message-resolver",
        move |ctx| {
            callback_flag.store(true, Ordering::SeqCst);
            Box::pin(async move {
                let name = format!("chat-rest-{}", unique_suffix());
                let chat = retry_definitive_rate_limit("REST chat workflow setup chat", || async {
                    ctx.client
                        .chats()
                        .in_space(&ctx.space_id)
                        .create(
                            &name,
                            Icon::Emoji {
                                emoji: "💬".to_string(),
                            },
                        )
                        .create()
                        .await
                })
                .await?;
                ctx.register_object(&chat.id);

                // Publishing remains gRPC so structured blocks are not discarded.
                let message_id = bounded_api(
                    "gRPC message creation",
                    ctx.client
                        .chats()
                        .add_message(&chat.id)
                        .content(MessageContent::new().bold("migration coverage"))
                        .blocks(vec![MessageBlock::Text(MessageBlockText {
                            text: "structured heading".to_string(),
                            style: MessageTextStyle::Header2,
                            ..MessageBlockText::default()
                        })])
                        .send(),
                )
                .await?;
                ctx.register_chat_message(&chat.id, &message_id)?;

                let rich = ctx
                    .client
                    .chats()
                    .get_messages(&chat.id, [&message_id])
                    .get()
                    .await?;
                assert_eq!(rich.len(), 1);
                assert_eq!(rich[0].content.text, "migration coverage");
                assert!(!rich[0].content.marks.is_empty());
                assert!(!rich[0].blocks.is_empty());

                let chats = ctx.client.chats().in_space(&ctx.space_id);
                let plain = chats.get_message(&chat.id, &message_id).get().await?;
                assert_eq!(plain.content.text, "migration coverage");
                assert!(plain.blocks.is_empty(), "REST does not expose blocks");

                let listed = chats.list_messages(&chat.id).limit(20).list().await?;
                assert!(listed.iter().any(|message| message.id == message_id));

                let rich_page = bounded_api(
                    "gRPC message order inventory",
                    ctx.client
                        .chats()
                        .list_messages(&chat.id)
                        .limit(20)
                        .list_page(),
                )
                .await?;
                let order_id = rich_page
                    .messages
                    .iter()
                    .find(|message| message.id == message_id)
                    .map(|message| message.order_id.clone())
                    .ok_or_else(|| TestError::Assertion {
                        message: "created message was absent from the bounded gRPC page".to_owned(),
                    })?;
                assert_ne!(order_id, message_id);

                let by_order = bounded_api(
                    "message order-id resolution",
                    ctx.client.resolve_message_id(&chat.id, &order_id),
                )
                .await?;
                assert_eq!(by_order, message_id);
                let direct = bounded_api(
                    "direct message-id pass-through",
                    ctx.client.resolve_message_id(&chat.id, &message_id),
                )
                .await?;
                assert_eq!(direct, message_id);
                let batch = bounded_api(
                    "mixed message-id resolution",
                    ctx.client
                        .resolve_message_ids(&chat.id, &[order_id, message_id.clone()]),
                )
                .await?;
                assert_eq!(batch, vec![message_id.clone(), message_id.clone()]);

                let unknown_order_id = format!("unknown-order-{}", unique_suffix());
                let unknown_result = timeout(
                    LIVE_OPERATION_TIMEOUT,
                    ctx.client.resolve_message_id(&chat.id, &unknown_order_id),
                )
                .await
                .map_err(|_| TestError::Assertion {
                    message:
                        "unknown message order-id resolution exceeded its fixed live-test timeout"
                            .to_owned(),
                })?;
                assert!(matches!(unknown_result, Err(AnytypeError::NotFound { .. })));

                Ok(())
            })
        },
    ))
    .await
    .expect("cleanup-safe chat-message resolver live harness");
    assert_disposable_completed(outcome, &callback_ran, "chat-message resolver live suite");
}