vyctor 0.1.0

A fast CLI tool for semantic file search using vector embeddings
Documentation
//! Common test utilities and helpers

use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use tempfile::TempDir;

/// Check if DuckDB VSS extension is available
///
/// This is cached after the first check to avoid repeated initialization attempts.
pub fn vss_available() -> bool {
    static VSS_AVAILABLE: OnceLock<bool> = OnceLock::new();

    *VSS_AVAILABLE.get_or_init(|| match duckdb::Connection::open_in_memory() {
        Ok(conn) => conn.execute_batch("INSTALL vss; LOAD vss;").is_ok(),
        Err(_) => false,
    })
}

/// Macro to skip tests if VSS extension is not available
#[macro_export]
macro_rules! require_vss {
    () => {
        if !$crate::common::vss_available() {
            eprintln!("Skipping test: DuckDB VSS extension not available");
            return;
        }
    };
}

/// A temporary project directory for testing
///
/// Creates a temporary directory with optional test files.
/// Automatically cleaned up when dropped.
pub struct TempProject {
    dir: TempDir,
}

impl TempProject {
    /// Create a new empty temporary project
    pub fn new() -> Self {
        Self {
            dir: TempDir::new().expect("Failed to create temp directory"),
        }
    }

    /// Create a temporary project with sample files for testing
    pub fn with_sample_files() -> Self {
        let project = Self::new();
        project.create_sample_files();
        project
    }

    /// Get the path to the temporary directory
    pub fn path(&self) -> &Path {
        self.dir.path()
    }

    /// Get the path as a PathBuf
    #[allow(dead_code)]
    pub fn path_buf(&self) -> PathBuf {
        self.dir.path().to_path_buf()
    }

    /// Create a file with the given content
    pub fn create_file(&self, relative_path: &str, content: &str) {
        let path = self.dir.path().join(relative_path);
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).expect("Failed to create directories");
        }
        std::fs::write(&path, content).expect("Failed to write file");
    }

    /// Create a directory
    pub fn create_dir(&self, relative_path: &str) {
        let path = self.dir.path().join(relative_path);
        std::fs::create_dir_all(&path).expect("Failed to create directory");
    }

    /// Check if a file exists
    pub fn file_exists(&self, relative_path: &str) -> bool {
        self.dir.path().join(relative_path).exists()
    }

    /// Check if a directory exists
    pub fn dir_exists(&self, relative_path: &str) -> bool {
        self.dir.path().join(relative_path).is_dir()
    }

    /// Read a file's content
    pub fn read_file(&self, relative_path: &str) -> String {
        let path = self.dir.path().join(relative_path);
        std::fs::read_to_string(&path).expect("Failed to read file")
    }

    /// Create sample files for testing
    fn create_sample_files(&self) {
        // Create a Rust file
        self.create_file(
            "src/main.rs",
            r#"fn main() {
    println!("Hello, world!");
}

fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 2), 4);
    }
}
"#,
        );

        // Create a TypeScript file
        self.create_file(
            "src/utils.ts",
            r#"export function formatDate(date: Date): string {
    return date.toISOString();
}

export function parseJson<T>(json: string): T {
    return JSON.parse(json);
}

export class Logger {
    private prefix: string;

    constructor(prefix: string) {
        this.prefix = prefix;
    }

    log(message: string): void {
        console.log(`[${this.prefix}] ${message}`);
    }
}
"#,
        );

        // Create a Python file
        self.create_file(
            "scripts/helper.py",
            r#"def calculate_sum(numbers: list[int]) -> int:
    """Calculate the sum of a list of numbers."""
    return sum(numbers)

def process_data(data: dict) -> dict:
    """Process and transform input data."""
    result = {}
    for key, value in data.items():
        result[key.upper()] = str(value)
    return result

class DataProcessor:
    def __init__(self, name: str):
        self.name = name
    
    def process(self, items: list) -> list:
        return [item.strip() for item in items]
"#,
        );

        // Create a markdown file
        self.create_file(
            "README.md",
            r#"# Test Project

This is a test project for vyctor.

## Features

- Feature 1: Does something useful
- Feature 2: Does something else

## Installation

```bash
cargo install vyctor
```

## Usage

Run the main program:

```bash
cargo run
```
"#,
        );

        // Create a JSON config file
        self.create_file(
            "config.json",
            r#"{
    "name": "test-project",
    "version": "1.0.0",
    "settings": {
        "debug": true,
        "maxItems": 100
    }
}
"#,
        );

        // Create a file that should be excluded (node_modules)
        self.create_file("node_modules/some-package/index.js", "module.exports = {};");
    }

    /// Initialize vyctor in the project directory
    ///
    /// Creates .vyctor directory (for database) and vyctor.config.toml in project root
    pub fn init_vyctor(&self) {
        self.create_dir(".vyctor");
        self.create_file(
            "vyctor.config.toml",
            r#"[indexing]
include = ["**/*.rs", "**/*.ts", "**/*.py", "**/*.md", "**/*.json"]
exclude = ["**/node_modules/**", "**/target/**", "**/.git/**", "**/.vyctor/**"]
chunk_size = 1000
chunk_overlap = 200

[embedding]
provider = "local"
model = "sentence-transformers/all-MiniLM-L6-v2"
dimensions = 384
batch_size = 100

[embedding.openai]
api_key_env = "OPENAI_API_KEY"
base_url = "https://api.openai.com/v1"

[embedding.voyage]
api_key_env = "VOYAGE_API_KEY"
base_url = "https://api.voyageai.com/v1"

[embedding.local]
model = "sentence-transformers/all-MiniLM-L6-v2"
"#,
        );
    }

    /// Initialize vyctor with a mock-friendly config (smaller dimensions)
    #[allow(dead_code)]
    pub fn init_vyctor_mock(&self) {
        self.create_dir(".vyctor");
        self.create_file(
            "vyctor.config.toml",
            r#"[indexing]
include = ["**/*.rs", "**/*.ts", "**/*.py", "**/*.md", "**/*.json"]
exclude = ["**/node_modules/**", "**/target/**", "**/.git/**", "**/.vyctor/**"]
chunk_size = 500
chunk_overlap = 100

[embedding]
provider = "local"
model = "sentence-transformers/all-MiniLM-L6-v2"
dimensions = 384
batch_size = 10

[embedding.openai]
api_key_env = "OPENAI_API_KEY"
base_url = "https://api.openai.com/v1"

[embedding.voyage]
api_key_env = "VOYAGE_API_KEY"
base_url = "https://api.voyageai.com/v1"

[embedding.local]
model = "sentence-transformers/all-MiniLM-L6-v2"
"#,
        );
    }
}

impl Default for TempProject {
    fn default() -> Self {
        Self::new()
    }
}

/// Helper to create a config that uses the local embedder
#[allow(dead_code)]
pub fn mock_config() -> vyctor::config::VyctorConfig {
    vyctor::config::VyctorConfig {
        indexing: vyctor::config::IndexingConfig {
            include: vec![
                "**/*.rs".to_string(),
                "**/*.ts".to_string(),
                "**/*.py".to_string(),
                "**/*.md".to_string(),
            ],
            exclude: vec![
                "**/node_modules/**".to_string(),
                "**/target/**".to_string(),
                "**/.vyctor/**".to_string(),
            ],
            chunk_size: 500,
            chunk_overlap: 100,
            semantic_chunking: true,
            max_chunk_size: 3000,
        },
        embedding: vyctor::config::EmbeddingConfig {
            provider: vyctor::config::EmbeddingProvider::Local,
            dimensions: 384,
            batch_size: 10,
            ..Default::default()
        },
        ..Default::default()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_temp_project_creation() {
        let project = TempProject::new();
        assert!(project.path().exists());
    }

    #[test]
    fn test_temp_project_with_files() {
        let project = TempProject::with_sample_files();
        assert!(project.file_exists("src/main.rs"));
        assert!(project.file_exists("src/utils.ts"));
        assert!(project.file_exists("scripts/helper.py"));
        assert!(project.file_exists("README.md"));
    }

    #[test]
    fn test_create_file() {
        let project = TempProject::new();
        project.create_file("test.txt", "hello world");
        assert!(project.file_exists("test.txt"));
        assert_eq!(project.read_file("test.txt"), "hello world");
    }

    #[test]
    fn test_create_nested_file() {
        let project = TempProject::new();
        project.create_file("deep/nested/path/file.txt", "nested content");
        assert!(project.file_exists("deep/nested/path/file.txt"));
    }

    #[test]
    fn test_init_vyctor() {
        let project = TempProject::new();
        project.init_vyctor();
        assert!(project.file_exists("vyctor.config.toml"));
        assert!(project.dir_exists(".vyctor"));
    }

    #[test]
    fn test_vss_check_doesnt_panic() {
        // Just verify the check runs without panic
        let _ = vss_available();
    }
}