cached-context 0.3.0

File cache with diff tracking for AI coding agents
Documentation
//! Smoke tests that mirror the original TypeScript test/smoke.ts
//!
//! These tests verify all the core functionality matches the original implementation:
//! 1. First read returns full content
//! 2. Second read, no changes — should be cached
//! 3. Modify file, read again — should return diff
//! 4. Stats tracking
//! 5. Multi-session isolation
//! 6. Partial read (offset/limit) — first read
//! 7. Partial read unchanged — should return cache hit
//! 8. Partial read, changes OUTSIDE requested range — still cached
//! 9. Partial read, changes INSIDE requested range — should return content

use cached_context::{CacheConfig, CacheStore, FileReadResult};

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

/// Test 1: First read returns full content
#[tokio::test]
async fn test_smoke_first_read_full_content() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("example.ts");
    std::fs::write(
        &test_file,
        "function hello() {\n  console.log(\"hello world\");\n}\n",
    )
    .expect("Failed to write test file");

    let store = create_test_store(&temp_dir, "test-session-1").await;

    let result: FileReadResult = store
        .read_file(test_file.to_str().unwrap(), None, None, false)
        .await
        .expect("Failed to read file");

    assert!(!result.cached, "First read should not be cached");
    assert_eq!(result.total_lines, 3, "Should have 3 lines");
    assert!(
        result.content.contains("function hello()"),
        "Should contain function definition"
    );
    assert!(
        result.content.contains("hello world"),
        "Should contain console.log"
    );
}

/// Test 2: Second read, no changes — should be cached
#[tokio::test]
async fn test_smoke_second_read_cached() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("example.ts");
    std::fs::write(
        &test_file,
        "function hello() {\n  console.log(\"hello world\");\n}\n",
    )
    .expect("Failed to write test file");

    let store = create_test_store(&temp_dir, "test-session-1").await;

    let _ = store
        .read_file(test_file.to_str().unwrap(), None, None, false)
        .await
        .expect("Failed to read file");

    let result: FileReadResult = store
        .read_file(test_file.to_str().unwrap(), None, None, false)
        .await
        .expect("Failed to read file");

    assert!(result.cached, "Second read should be cached");
    assert_eq!(result.lines_changed, None, "No lines should have changed");
    assert!(
        result.content.contains("unchanged"),
        "Should indicate unchanged: {}",
        result.content
    );
}

/// Test 3: Modify file, read again — should return diff
#[tokio::test]
async fn test_smoke_modified_file_returns_diff() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("example.ts");
    std::fs::write(
        &test_file,
        "function hello() {\n  console.log(\"hello world\");\n}\n",
    )
    .expect("Failed to write test file");

    let store = create_test_store(&temp_dir, "test-session-1").await;

    let _ = store
        .read_file(test_file.to_str().unwrap(), None, None, false)
        .await
        .expect("Failed to read file");

    std::fs::write(
        &test_file,
        "function hello() {\n  console.log(\"hello cachebro!\");\n  return true;\n}\n",
    )
    .expect("Failed to modify test file");

    let result: FileReadResult = store
        .read_file(test_file.to_str().unwrap(), None, None, false)
        .await
        .expect("Failed to read file");

    assert!(result.cached, "Should be cached (returning diff)");
    assert!(
        result.lines_changed.unwrap() > 0,
        "Should have changed lines"
    );
    assert!(result.diff.is_some(), "Should have diff content");
    let diff = result.diff.unwrap();
    assert!(
        diff.contains("---") || diff.contains("+++") || diff.contains("@@"),
        "Diff should contain markers: {}",
        diff
    );
}

/// Test 4: Stats tracking
#[tokio::test]
async fn test_smoke_stats_tracking() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("example.ts");
    std::fs::write(&test_file, "function hello() {}\n").expect("Failed to write test file");

    let store = create_test_store(&temp_dir, "test-session-1").await;

    let _ = store
        .read_file(test_file.to_str().unwrap(), None, None, false)
        .await
        .expect("Failed to read file");

    let _ = store
        .read_file(test_file.to_str().unwrap(), None, None, false)
        .await
        .expect("Failed to read file");

    let stats = store.get_stats().await.expect("Failed to get stats");

    assert_eq!(stats.files_tracked, 1, "Should track 1 file");
    assert!(
        stats.session_tokens_saved > 0,
        "Should have saved tokens in session"
    );
    assert!(stats.tokens_saved > 0, "Should have saved tokens total");
}

/// Test 5: Multi-session isolation
#[tokio::test]
async fn test_smoke_multi_session_isolation() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("example.ts");
    std::fs::write(&test_file, "function hello() {}\n").expect("Failed to write test file");

    let db_path = temp_dir.path().join("test.db");

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

    let _ = store1
        .read_file(test_file.to_str().unwrap(), None, None, false)
        .await
        .expect("Failed to read file");

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

    let result2 = store2
        .read_file(test_file.to_str().unwrap(), None, None, false)
        .await
        .expect("Failed to read file");

    assert!(!result2.cached, "Session 2 first read should NOT be cached");

    let result2b = store2
        .read_file(test_file.to_str().unwrap(), None, None, false)
        .await
        .expect("Failed to read file");

    assert!(result2b.cached, "Session 2 second read should be cached");
    assert_eq!(
        result2b.lines_changed, None,
        "No lines should have changed for session 2"
    );
}

/// Test 6: Partial read (offset/limit) — first read
#[tokio::test]
async fn test_smoke_partial_read_first_time() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("long.ts");

    let content: String = (0..20)
        .map(|i| format!("line {}: const x{} = {};", i + 1, i, i))
        .collect::<Vec<_>>()
        .join("\n");
    std::fs::write(&test_file, content).expect("Failed to write test file");

    let store = create_test_store(&temp_dir, "test-session-1").await;

    let result: FileReadResult = store
        .read_file(test_file.to_str().unwrap(), Some(5), Some(3), false)
        .await
        .expect("Failed to read file");

    assert!(!result.cached, "First partial read should not be cached");
    assert!(result.content.contains("line 5"), "Should include line 5");
    assert!(result.content.contains("line 7"), "Should include line 7");
    assert!(
        !result.content.contains("line 8"),
        "Should NOT include line 8"
    );
}

/// Test 7: Partial read unchanged — should return cache hit
#[tokio::test]
async fn test_smoke_partial_read_unchanged() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("long.ts");

    let content: String = (0..20)
        .map(|i| format!("line {}: const x{} = {};", i + 1, i, i))
        .collect::<Vec<_>>()
        .join("\n");
    std::fs::write(&test_file, content).expect("Failed to write test file");

    let store = create_test_store(&temp_dir, "test-session-1").await;

    let _ = store
        .read_file(test_file.to_str().unwrap(), Some(5), Some(3), false)
        .await
        .expect("Failed to read file");

    let result: FileReadResult = store
        .read_file(test_file.to_str().unwrap(), Some(5), Some(3), false)
        .await
        .expect("Failed to read file");

    assert!(result.cached, "Second partial read should be cached");
    assert_eq!(result.lines_changed, None, "No lines should have changed");
    assert!(
        result.content.contains("unchanged"),
        "Should say unchanged: {}",
        result.content
    );
}

/// Test 8: Partial read, changes OUTSIDE requested range — still cached
#[tokio::test]
async fn test_smoke_partial_read_changes_outside_range() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("long.ts");

    let mut lines: Vec<String> = (0..20)
        .map(|i| format!("line {}: const x{} = {};", i + 1, i, i))
        .collect();
    let content = lines.join("\n");
    std::fs::write(&test_file, &content).expect("Failed to write test file");

    let store = create_test_store(&temp_dir, "test-session-1").await;

    let _ = store
        .read_file(test_file.to_str().unwrap(), Some(5), Some(3), false)
        .await
        .expect("Failed to read file");

    lines[0] = "line 1: MODIFIED".to_string();
    lines[18] = "line 19: MODIFIED".to_string();
    let modified_content = lines.join("\n");
    std::fs::write(&test_file, modified_content).expect("Failed to modify test file");

    let result: FileReadResult = store
        .read_file(test_file.to_str().unwrap(), Some(5), Some(3), false)
        .await
        .expect("Failed to read file");

    assert!(
        result.cached,
        "Should be cached — changes outside requested range"
    );
    assert_eq!(
        result.lines_changed,
        Some(0),
        "Should report 0 lines changed in range"
    );
    assert!(
        result.content.contains("unchanged"),
        "Should say unchanged: {}",
        result.content
    );
}

/// Test 9: Partial read, changes INSIDE requested range — should return content
#[tokio::test]
async fn test_smoke_partial_read_changes_inside_range() {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let test_file = temp_dir.path().join("long.ts");

    let mut lines: Vec<String> = (0..20)
        .map(|i| format!("line {}: const x{} = {};", i + 1, i, i))
        .collect();
    let content = lines.join("\n");
    std::fs::write(&test_file, &content).expect("Failed to write test file");

    let store = create_test_store(&temp_dir, "test-session-1").await;

    let _ = store
        .read_file(test_file.to_str().unwrap(), Some(5), Some(3), false)
        .await
        .expect("Failed to read file");

    lines[5] = "line 6: MODIFIED_IN_RANGE".to_string();
    let modified_content = lines.join("\n");
    std::fs::write(&test_file, modified_content).expect("Failed to modify test file");

    let result: FileReadResult = store
        .read_file(test_file.to_str().unwrap(), Some(5), Some(3), false)
        .await
        .expect("Failed to read file");

    assert!(
        !result.cached,
        "Should NOT be cached — changes inside requested range"
    );
    assert!(
        result.content.contains("MODIFIED_IN_RANGE"),
        "Should include modified content: {}",
        result.content
    );
}