bamboo-server 2026.4.24

HTTP server and API layer for the Bamboo agent framework
Documentation
use crate::app_state::AgentStatus;
use bamboo_agent_core::{AgentEvent, Message, Session};

use crate::session_app::execute::{
    consume_pending_conclusion_with_options_resume, has_pending_user_message,
};
use bamboo_engine::execution::agent_spawn::{
    preserve_concurrent_session_overrides, terminal_error_event_for_result,
};

use super::session_state::{selected_skill_ids_for_session, selected_skill_mode_for_session};
use crate::app_state::runner_lifecycle::status_from_execution_result;

#[test]
fn has_pending_user_message_only_when_last_message_is_user() {
    let mut session = Session::new("session-1", "gpt-4o-mini");
    session.add_message(Message::system("sys"));
    session.add_message(Message::user("hello"));
    assert!(has_pending_user_message(&session));

    session.add_message(Message::assistant("done", None));
    assert!(!has_pending_user_message(&session));
}

#[test]
fn has_pending_user_message_when_conclusion_with_options_resume_is_marked() {
    let mut session = Session::new("session-1", "gpt-4o-mini");
    session.add_message(Message::assistant("tool question", None));
    session.add_message(Message::tool_result("ask-1", "User selected: A"));
    assert!(!has_pending_user_message(&session));

    session.metadata.insert(
        "conclusion_with_options_resume_pending".to_string(),
        "true".to_string(),
    );
    assert!(has_pending_user_message(&session));

    consume_pending_conclusion_with_options_resume(&mut session);
    assert!(!has_pending_user_message(&session));
}

#[test]
fn has_pending_user_message_when_error_retry_resume_is_marked() {
    let mut session = Session::new("session-1", "gpt-4o-mini");
    session.add_message(Message::user("hello"));
    session.add_message(Message::assistant("failed with rate limit", None));
    assert!(!has_pending_user_message(&session));

    session
        .metadata
        .insert("retry_resume_pending".to_string(), "true".to_string());
    session
        .metadata
        .insert("retry_resume_reason".to_string(), "error_retry".to_string());
    assert!(has_pending_user_message(&session));

    consume_pending_conclusion_with_options_resume(&mut session);
    assert!(!has_pending_user_message(&session));
    assert!(!session.metadata.contains_key("retry_resume_pending"));
    assert!(!session.metadata.contains_key("retry_resume_reason"));
}

#[test]
fn selected_skill_ids_for_session_parses_metadata_json() {
    let mut session = Session::new("session-1", "gpt-4o-mini");
    session.metadata.insert(
        "selected_skill_ids".to_string(),
        "[\"pdf\",\"skill-creator\"]".to_string(),
    );

    assert_eq!(
        selected_skill_ids_for_session(&session),
        Some(vec!["pdf".to_string(), "skill-creator".to_string()])
    );
}

#[test]
fn selected_skill_mode_for_session_prefers_skill_mode_key() {
    let mut session = Session::new("session-1", "gpt-4o-mini");
    session
        .metadata
        .insert("mode".to_string(), "ask".to_string());
    session
        .metadata
        .insert("skill_mode".to_string(), "code".to_string());

    assert_eq!(
        selected_skill_mode_for_session(&session).as_deref(),
        Some("code")
    );
}

#[test]
fn execution_result_mapping_handles_cancelled_and_error_states() {
    let ok_result: anyhow::Result<()> = Ok(());
    assert!(matches!(
        status_from_execution_result(&ok_result),
        AgentStatus::Completed
    ));

    let cancelled_result: anyhow::Result<()> = Err(anyhow::anyhow!("request cancelled"));
    assert!(matches!(
        status_from_execution_result(&cancelled_result),
        AgentStatus::Cancelled
    ));

    let error_result: anyhow::Result<()> = Err(anyhow::anyhow!("boom"));
    match status_from_execution_result(&error_result) {
        AgentStatus::Error(message) => assert!(message.contains("boom")),
        other => panic!("unexpected status: {other:?}"),
    }
}

#[test]
fn terminal_error_event_mapping_matches_execution_result() {
    let ok_result: anyhow::Result<()> = Ok(());
    assert!(terminal_error_event_for_result(&ok_result).is_none());

    let cancelled_result: anyhow::Result<()> = Err(anyhow::anyhow!("cancelled by user"));
    match terminal_error_event_for_result(&cancelled_result) {
        Some(AgentEvent::Error { message }) => {
            assert_eq!(message, "Agent execution cancelled by user");
        }
        other => panic!("unexpected event: {other:?}"),
    }

    let error_result: anyhow::Result<()> = Err(anyhow::anyhow!("network failed"));
    match terminal_error_event_for_result(&error_result) {
        Some(AgentEvent::Error { message }) => assert!(message.contains("network failed")),
        other => panic!("unexpected event: {other:?}"),
    }
}

#[test]
fn preserve_concurrent_session_overrides_applies_latest_title_and_pin_when_unchanged_in_execution()
{
    let mut running_snapshot = Session::new("session-1", "gpt-4o-mini");
    running_snapshot.title = "New Session".to_string();
    running_snapshot.pinned = false;

    let mut latest_persisted = running_snapshot.clone();
    latest_persisted.title = "Debug websocket reconnect issue".to_string();
    latest_persisted.pinned = true;

    preserve_concurrent_session_overrides(
        &mut running_snapshot,
        &latest_persisted,
        "New Session",
        false,
    );

    assert_eq!(running_snapshot.title, latest_persisted.title);
    assert!(running_snapshot.pinned);
}

#[test]
fn preserve_concurrent_session_overrides_keeps_execution_changes() {
    let mut running_snapshot = Session::new("session-1", "gpt-4o-mini");
    running_snapshot.title = "Execution-assigned title".to_string();
    running_snapshot.pinned = true;

    let mut latest_persisted = running_snapshot.clone();
    latest_persisted.title = "User edited title".to_string();
    latest_persisted.pinned = false;

    preserve_concurrent_session_overrides(
        &mut running_snapshot,
        &latest_persisted,
        "New Session",
        false,
    );

    assert_eq!(running_snapshot.title, "Execution-assigned title");
    assert!(running_snapshot.pinned);
}