robson-core 0.1.0

Rust async agent orchestrator for automated development workflows
Documentation
use robson_core::conversation::{ConversationRole, Model as Conversation};
use robson_core::process_event::Model as ProcessEvent;

async fn setup_db() -> sea_orm::DatabaseConnection {
    let db = robson_core::connect(":memory:").await.unwrap();
    robson_core::run_migrations(&db).await.unwrap();
    db
}

// ── Conversation::find_unprocessed ────────────────────────────────────────────

#[tokio::test]
async fn test_find_unprocessed_returns_only_new_rows() {
    let db = setup_db().await;

    // Insert a row — should come back unprocessed
    Conversation::insert(&db, None, "C1", "", "U1", ConversationRole::User, "hello")
        .await
        .unwrap();
    let rows = Conversation::find_unprocessed(&db).await.unwrap();
    assert_eq!(rows.len(), 1);
    assert_eq!(rows[0].content, "hello");
    assert!(!rows[0].processed);
}

#[tokio::test]
async fn test_find_unprocessed_skips_processed_rows() {
    let db = setup_db().await;

    Conversation::insert(&db, None, "C1", "", "U1", ConversationRole::User, "msg1")
        .await
        .unwrap();
    let rows = Conversation::find_unprocessed(&db).await.unwrap();
    let id = rows[0].id;

    Conversation::mark_processed(&db, id).await.unwrap();

    let after = Conversation::find_unprocessed(&db).await.unwrap();
    assert!(after.is_empty());
}

#[tokio::test]
async fn test_find_unprocessed_returns_multiple_rows() {
    let db = setup_db().await;

    for i in 0..3 {
        Conversation::insert(
            &db,
            None,
            "C1",
            "",
            "U1",
            ConversationRole::User,
            &format!("msg{i}"),
        )
        .await
        .unwrap();
    }

    let rows = Conversation::find_unprocessed(&db).await.unwrap();
    assert_eq!(rows.len(), 3);
}

#[tokio::test]
async fn test_find_unprocessed_empty_when_no_rows() {
    let db = setup_db().await;
    let rows = Conversation::find_unprocessed(&db).await.unwrap();
    assert!(rows.is_empty());
}

// ── Conversation::mark_processed ──────────────────────────────────────────────

#[tokio::test]
async fn test_mark_processed_sets_flag() {
    let db = setup_db().await;

    Conversation::insert(&db, None, "C1", "", "U1", ConversationRole::User, "hi")
        .await
        .unwrap();
    let rows = Conversation::find_unprocessed(&db).await.unwrap();
    let id = rows[0].id;

    Conversation::mark_processed(&db, id).await.unwrap();

    // find_unprocessed should now return nothing
    let remaining = Conversation::find_unprocessed(&db).await.unwrap();
    assert!(remaining.is_empty());
}

#[tokio::test]
async fn test_mark_processed_only_marks_targeted_row() {
    let db = setup_db().await;

    Conversation::insert(&db, None, "C1", "", "U1", ConversationRole::User, "first")
        .await
        .unwrap();
    Conversation::insert(&db, None, "C1", "", "U1", ConversationRole::User, "second")
        .await
        .unwrap();

    let rows = Conversation::find_unprocessed(&db).await.unwrap();
    assert_eq!(rows.len(), 2);

    Conversation::mark_processed(&db, rows[0].id).await.unwrap();

    let remaining = Conversation::find_unprocessed(&db).await.unwrap();
    assert_eq!(remaining.len(), 1);
    assert_eq!(remaining[0].content, "second");
}

// ── ProcessEvent::insert / find_by_conversation ───────────────────────────────

#[tokio::test]
async fn test_process_event_insert_and_find() {
    let db = setup_db().await;

    ProcessEvent::insert(&db, 1, "started", "workflow started")
        .await
        .unwrap();
    let rows = ProcessEvent::find_by_conversation(&db, 1).await.unwrap();
    assert_eq!(rows.len(), 1);
    assert_eq!(rows[0].kind, "started");
    assert_eq!(rows[0].content, "workflow started");
}

#[tokio::test]
async fn test_process_event_find_returns_empty_for_unknown_conversation() {
    let db = setup_db().await;
    let rows = ProcessEvent::find_by_conversation(&db, 999).await.unwrap();
    assert!(rows.is_empty());
}

#[tokio::test]
async fn test_process_event_multiple_kinds_persisted() {
    let db = setup_db().await;

    ProcessEvent::insert(&db, 5, "started", "begin")
        .await
        .unwrap();
    ProcessEvent::insert(&db, 5, "progress", "halfway")
        .await
        .unwrap();
    ProcessEvent::insert(&db, 5, "completed", "done")
        .await
        .unwrap();

    let rows = ProcessEvent::find_by_conversation(&db, 5).await.unwrap();
    assert_eq!(rows.len(), 3);
    let kinds: Vec<&str> = rows.iter().map(|r| r.kind.as_str()).collect();
    assert_eq!(kinds, vec!["started", "progress", "completed"]);
}

#[tokio::test]
async fn test_process_event_isolated_per_conversation() {
    let db = setup_db().await;

    ProcessEvent::insert(&db, 1, "started", "a").await.unwrap();
    ProcessEvent::insert(&db, 2, "failed", "b").await.unwrap();

    let rows1 = ProcessEvent::find_by_conversation(&db, 1).await.unwrap();
    let rows2 = ProcessEvent::find_by_conversation(&db, 2).await.unwrap();

    assert_eq!(rows1.len(), 1);
    assert_eq!(rows2.len(), 1);
    assert_eq!(rows1[0].kind, "started");
    assert_eq!(rows2[0].kind, "failed");
}