cached-context 0.3.0

File cache with diff tracking for AI coding agents
Documentation
//! Integration tests for cached-context error handling
//!
//! These tests verify the error handling works correctly

use cached_context::{CacheConfig, CacheStore, Error};
use rusqlite::params;

/// Helper to create a test cache store
async fn create_test_store(temp_dir: &tempfile::TempDir) -> CacheStore {
    let config = CacheConfig {
        db_path: temp_dir.path().join("test_cache.db"),
        session_id: "test-session".to_string(),
        workdir: temp_dir.path().to_path_buf(),
    };
    let store = CacheStore::new(config).expect("Failed to create cache store");
    store.init().await.expect("Failed to init cache store");
    store
}

#[tokio::test]
async fn test_read_nonexistent_file() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let store = create_test_store(&temp_dir).await;

    let result = store.read_file("nonexistent.txt", None, None, false).await;
    
    assert!(result.is_err());
    match result {
        Err(Error::FileNotFound(path)) => {
            assert!(path.contains("nonexistent.txt"));
        }
        Err(e) => panic!("Expected FileNotFound error, got: {:?}", e),
        Ok(_) => panic!("Expected error, got success"),
    }
}

#[tokio::test]
async fn test_read_file_with_invalid_path() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let store = create_test_store(&temp_dir).await;

    let result = store.read_file("/invalid/\0path", None, None, false).await;
    
    assert!(result.is_err());
}

#[tokio::test]
async fn test_mcp_read_nonexistent_file() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let store = create_test_store(&temp_dir).await;
    let service = cached_context::mcp::CachebroMcpService::new(store);

    let result = service
        .read_file("/nonexistent/file.txt".to_string(), None, None, false)
        .await
        .expect("read_file should not panic");

    let text = result
        .content
        .first()
        .and_then(|c| c.as_text())
        .map(|t| t.text.clone())
        .unwrap_or_default();

    assert!(
        text.to_lowercase().contains("error") || text.to_lowercase().contains("not found"),
        "Error response should mention error or not found, got: {}",
        text
    );
}

#[tokio::test]
async fn test_cache_operations_after_clear() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("test.txt");
    std::fs::write(&test_file, "test content").expect("Failed to write test file");

    let store = create_test_store(&temp_dir).await;

    let result1 = store.read_file("test.txt", None, None, false).await.expect("First read should work");
    assert!(!result1.cached);

    store.clear().await.expect("Clear should work");

    let result2 = store.read_file("test.txt", None, None, false).await.expect("Read after clear should work");
    assert!(!result2.cached, "After clear, file should not be cached");
}

#[tokio::test]
async fn test_multiple_sessions_isolated() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("test.txt");
    std::fs::write(&test_file, "test content").expect("Failed to write test file");

    let config1 = CacheConfig {
        db_path: temp_dir.path().join("test_cache.db"),
        session_id: "session-1".to_string(),
        workdir: temp_dir.path().to_path_buf(),
    };
    let store1 = CacheStore::new(config1).expect("Failed to create store1");
    store1.init().await.expect("Failed to init store1");

    let config2 = CacheConfig {
        db_path: temp_dir.path().join("test_cache.db"),
        session_id: "session-2".to_string(),
        workdir: temp_dir.path().to_path_buf(),
    };
    let store2 = CacheStore::new(config2).expect("Failed to create store2");
    store2.init().await.expect("Failed to init store2");

    let result1 = store1.read_file("test.txt", None, None, false).await.expect("Session 1 read should work");
    assert!(!result1.cached);

    let _result2 = store2.read_file("test.txt", None, None, false).await.expect("Session 2 read should work");
}

#[tokio::test]
async fn test_token_estimation_savings() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("test.txt");
    std::fs::write(&test_file, "test content for token test").expect("Failed to write test file");

    let store = create_test_store(&temp_dir).await;

    let _result1 = store.read_file("test.txt", None, None, false).await.expect("First read should work");

    let stats1 = store.get_stats().await.expect("Get stats should work");
    assert!(
        stats1.files_tracked > 0,
        "Should track the file after first read, got: {}",
        stats1.files_tracked
    );
}

#[tokio::test]
async fn test_partial_read_offset_limit() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("test.txt");
    std::fs::write(
        &test_file,
        "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n",
    )
    .expect("Failed to write test file");

    let store = create_test_store(&temp_dir).await;

    let result = store
        .read_file("test.txt", Some(3), Some(2), false)
        .await
        .expect("Read with offset/limit should work");

    assert_eq!(result.content, "line3\nline4");
    assert_eq!(result.total_lines, 10);
}

#[tokio::test]
async fn test_force_read_bypasses_cache() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("test.txt");
    std::fs::write(&test_file, "original content").expect("Failed to write test file");

    let store = create_test_store(&temp_dir).await;

    let result1 = store.read_file("test.txt", None, None, false).await.expect("First read should work");
    assert!(!result1.cached);

    let result2 = store
        .read_file("test.txt", None, None, true)
        .await
        .expect("Force read should work");
    
    assert!(result2.content.contains("original content"));
}

#[tokio::test]
async fn test_changed_file_old_version_missing() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("test.txt");
    std::fs::write(&test_file, "original content\nline 2\n").expect("Failed to write test file");

    let db_path = temp_dir.path().join("test_cache.db");
    let config = CacheConfig {
        db_path: db_path.clone(),
        session_id: "test-session".to_string(),
        workdir: temp_dir.path().to_path_buf(),
    };
    let store = CacheStore::new(config).expect("Failed to create cache store");
    store.init().await.expect("Failed to init cache store");

    let result1 = store
        .read_file("test.txt", None, None, false)
        .await
        .expect("First read should work");
    assert!(!result1.cached);
    let original_hash = result1.hash.clone();

    let conn = rusqlite::Connection::open(&db_path).expect("Failed to open DB");
    conn.execute(
        "DELETE FROM file_versions WHERE path = ? AND hash = ?",
        params!["test.txt", original_hash],
    )
    .expect("Failed to delete old version");
    drop(conn);

    std::fs::write(&test_file, "modified content\nline 2\nline 3\n")
        .expect("Failed to modify test file");

    let result2 = store
        .read_file("test.txt", None, None, false)
        .await
        .expect("Read with missing old version should work");

    assert!(
        !result2.cached,
        "Should NOT be cached when old version is missing"
    );
    assert!(
        result2.content.contains("modified content"),
        "Should return the new full content, got: {}",
        result2.content
    );
    assert!(
        result2.diff.is_none(),
        "Should NOT have a diff when old version is missing"
    );
}