oxi-cli 0.26.1

Terminal-based AI coding assistant — multi-provider, streaming-first, extensible
Documentation
//! Session persistence tests — verifies create → persist → reload cycle.

use oxi_store::session::{AgentMessage, AssistantContentBlock, ContentValue, SessionManager};
use tempfile::TempDir;

fn make_user_message(text: &str) -> AgentMessage {
    AgentMessage::User {
        content: ContentValue::String(text.to_string()),
    }
}

fn make_assistant_message(text: &str) -> AgentMessage {
    AgentMessage::Assistant {
        content: vec![AssistantContentBlock::Text {
            text: text.to_string(),
        }],
        provider: Some("mock".to_string()),
        model_id: Some("mock/model".to_string()),
        usage: None,
        stop_reason: None,
    }
}

#[test]
fn test_create_and_load_session() {
    let dir = TempDir::new().unwrap();
    let dir_path = dir.path().to_str().unwrap();

    let mut manager = SessionManager::create(".", Some(dir_path));
    let id = manager.get_session_id();
    assert!(!id.is_empty());

    let session_file = manager
        .get_session_file()
        .expect("session file should be set");

    // User message alone doesn't flush — assistant message triggers persistence
    manager.append_message(make_user_message("hello"));
    manager.append_message(make_assistant_message("world"));

    assert!(
        std::path::Path::new(&session_file).exists(),
        "session file should exist after assistant message"
    );
}

#[test]
fn test_session_file_is_valid_jsonl() {
    let dir = TempDir::new().unwrap();
    let dir_path = dir.path().to_str().unwrap();

    let mut manager = SessionManager::create("/tmp/test", Some(dir_path));
    manager.append_message(make_user_message("hello"));
    manager.append_message(make_assistant_message("response"));

    let session_file = manager.get_session_file().unwrap();
    let contents = std::fs::read_to_string(&session_file).unwrap();

    // Every non-empty line must be valid JSON
    for (i, line) in contents.lines().enumerate() {
        if line.trim().is_empty() {
            continue;
        }
        serde_json::from_str::<serde_json::Value>(line)
            .unwrap_or_else(|e| panic!("line {} is not valid JSON: {} — {}", i, e, line));
    }
}

#[test]
fn test_session_roundtrip_preserves_assistant_content() {
    let dir = TempDir::new().unwrap();
    let dir_path = dir.path().to_str().unwrap();

    let mut manager = SessionManager::create("/tmp/test", Some(dir_path));
    let _uid = manager.append_message(make_user_message("question"));
    let aid = manager.append_message(make_assistant_message("answer"));

    let session_file = manager.get_session_file().unwrap();

    // Reload
    let loaded = SessionManager::open(&session_file, Some(dir_path), None);
    let entry = loaded.get_entry(&aid);
    assert!(entry.is_some(), "assistant entry should survive roundtrip");
    assert_eq!(entry.unwrap().content(), "answer");
}

#[test]
fn test_no_temp_files_left_behind() {
    let dir = TempDir::new().unwrap();
    let dir_path = dir.path().to_str().unwrap();

    let mut manager = SessionManager::create("/tmp/test", Some(dir_path));
    manager.append_message(make_user_message("msg"));
    manager.append_message(make_assistant_message("reply"));

    // Check for leftover temp files
    let tmp_files: Vec<_> = std::fs::read_dir(dir_path)
        .unwrap()
        .filter_map(|e| e.ok())
        .filter(|e| {
            e.path()
                .extension()
                .map(|ext| ext.to_string_lossy().starts_with("tmp"))
                .unwrap_or(false)
        })
        .collect();

    assert!(
        tmp_files.is_empty(),
        "no temp files should remain: {:?}",
        tmp_files
    );
}

#[test]
fn test_in_memory_session_has_no_file() {
    let manager = SessionManager::in_memory("/tmp/test");
    assert!(!manager.is_persisted());
    assert!(manager.get_session_file().is_none());
}

#[test]
fn test_multiple_messages_persist() {
    let dir = TempDir::new().unwrap();
    let dir_path = dir.path().to_str().unwrap();

    let mut manager = SessionManager::create("/tmp/test", Some(dir_path));

    // Write multiple exchanges
    for i in 0..5 {
        manager.append_message(make_user_message(&format!("user msg {}", i)));
        manager.append_message(make_assistant_message(&format!("assistant msg {}", i)));
    }

    let session_file = manager.get_session_file().unwrap();
    let contents = std::fs::read_to_string(&session_file).unwrap();
    let line_count = contents.lines().filter(|l| !l.trim().is_empty()).count();

    // At least header + 5 assistant messages (user messages may not persist due to
    // #[serde(flatten)] serialization issues in AgentMessage::User)
    assert!(
        line_count >= 6,
        "expected at least 6 lines, got {}",
        line_count
    );
}