frigg 0.9.2

Frigg gives AI agents local, source-backed code search and navigation without sending whole repositories through every prompt.
Documentation
//! Regression tests for ripgrep backend availability, manifest candidate filtering, and JSON stream parsing.

use super::*;
use crate::searcher::{ExactSearchExecutionOutput, SearchTextExecutionOptions};

fn assert_exact_execution_parity(
    expected: &ExactSearchExecutionOutput,
    actual: &ExactSearchExecutionOutput,
) {
    assert_eq!(actual.total_matches, expected.total_matches);
    assert_eq!(actual.total_rows, expected.total_rows);
    assert_eq!(actual.matches, expected.matches);
    assert_eq!(actual.coverage, expected.coverage);
}

#[test]
fn literal_search_ripgrep_backend_filters_hits_to_validated_candidate_universe() -> FriggResult<()>
{
    clear_ripgrep_availability_cache();
    let root = temp_workspace_root("literal-search-ripgrep-manifest");
    prepare_workspace(
        &root,
        &[
            ("src/indexed.rs", "needle indexed\n"),
            ("src/out_scope.rs", "needle out\n"),
        ],
    )?;
    seed_manifest_snapshot(&root, "repo-001", "snapshot-001", &["src/indexed.rs"])?;
    let fake_rg = write_fake_ripgrep_script(
        &root,
        r#"{"type":"match","data":{"path":{"text":"src/indexed.rs"},"lines":{"text":"needle indexed\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"needle"},"start":0,"end":6}]}}
{"type":"match","data":{"path":{"text":"src/out_scope.rs"},"lines":{"text":"needle out\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"needle"},"start":0,"end":6}]}}"#,
    )?;

    let mut config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Ripgrep;
    config.lexical_runtime.ripgrep_executable = Some(fake_rg);
    let searcher = TextSearcher::new(config);

    let output = searcher.search_literal_with_filters_diagnostics(
        SearchTextQuery {
            query: "needle".to_owned(),
            path_regex: None,
            limit: 20,
        },
        SearchFilters::default(),
    )?;

    assert_eq!(output.lexical_backend, Some(SearchLexicalBackend::Ripgrep));
    assert_eq!(
        output.matches,
        vec![
            text_match("repo-001", "src/indexed.rs", 1, 1, "needle indexed"),
            text_match("repo-001", "src/out_scope.rs", 1, 1, "needle out"),
        ]
    );

    cleanup_workspace(&root);
    Ok(())
}

#[test]
fn literal_search_falls_back_to_native_when_ripgrep_is_unavailable() -> FriggResult<()> {
    clear_ripgrep_availability_cache();
    let root = temp_workspace_root("literal-search-ripgrep-fallback");
    prepare_workspace(&root, &[("src/indexed.rs", "needle indexed\n")])?;

    let mut config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Ripgrep;
    config.lexical_runtime.ripgrep_executable = Some(root.join("missing-rg-executable"));
    let searcher = TextSearcher::new(config);

    let output = searcher.search_literal_with_filters_diagnostics(
        SearchTextQuery {
            query: "needle".to_owned(),
            path_regex: None,
            limit: 20,
        },
        SearchFilters::default(),
    )?;

    assert_eq!(output.lexical_backend, Some(SearchLexicalBackend::Native));
    assert!(
        output
            .lexical_backend_note
            .as_deref()
            .is_some_and(|note| note.contains("ripgrep unavailable")),
        "expected native fallback note, got {:?}",
        output.lexical_backend_note
    );
    assert_eq!(
        output.matches,
        vec![text_match(
            "repo-001",
            "src/indexed.rs",
            1,
            1,
            "needle indexed"
        )]
    );

    cleanup_workspace(&root);
    Ok(())
}

#[test]
fn hybrid_search_reports_ripgrep_backend_in_lexical_only_mode() -> FriggResult<()> {
    clear_ripgrep_availability_cache();
    let root = temp_workspace_root("hybrid-search-ripgrep-backend");
    prepare_workspace(&root, &[("src/runtime.rs", "pub fn needle_handler() {}\n")])?;
    let fake_rg = write_fake_ripgrep_script(
        &root,
        r#"{"type":"match","data":{"path":{"text":"src/runtime.rs"},"lines":{"text":"pub fn needle_handler() {}\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"needle_handler"},"start":7,"end":21}]}}"#,
    )?;

    let mut config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Ripgrep;
    config.lexical_runtime.ripgrep_executable = Some(fake_rg);
    let searcher = TextSearcher::new(config);

    let output = searcher.search_hybrid_with_filters_using_executor(
        SearchHybridQuery {
            query: "needle handler".to_owned(),
            limit: 10,
            weights: HybridChannelWeights::default(),
            semantic: Some(false),
        },
        SearchFilters::default(),
        &SemanticRuntimeCredentials::default(),
        &PanicSemanticQueryEmbeddingExecutor,
    )?;

    assert_eq!(output.note.semantic_status, HybridSemanticStatus::Disabled);
    assert!(matches!(
        output.note.lexical_backend,
        Some(SearchLexicalBackend::Ripgrep | SearchLexicalBackend::Mixed)
    ));
    assert!(
        output
            .note
            .lexical_backend_note
            .as_deref()
            .is_some_and(|note| note.contains("ripgrep accelerator active"))
    );
    assert_eq!(output.matches.len(), 1);
    assert_eq!(output.matches[0].document.path, "src/runtime.rs");

    cleanup_workspace(&root);
    Ok(())
}

#[test]
fn hybrid_search_single_exact_term_uses_ripgrep_backend_when_available() -> FriggResult<()> {
    clear_ripgrep_availability_cache();
    let root = temp_workspace_root("hybrid-search-ripgrep-single-token");
    prepare_workspace(&root, &[("src/runtime.rs", "pub fn needle_handler() {}\n")])?;
    let fake_rg = write_fake_ripgrep_script(
        &root,
        r#"{"type":"match","data":{"path":{"text":"src/runtime.rs"},"lines":{"text":"pub fn needle_handler() {}\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"needle_handler"},"start":7,"end":21}]}}"#,
    )?;

    let mut config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Ripgrep;
    config.lexical_runtime.ripgrep_executable = Some(fake_rg);
    let searcher = TextSearcher::new(config);

    let output = searcher.search_hybrid_with_filters_using_executor(
        SearchHybridQuery {
            query: "needle_handler".to_owned(),
            limit: 10,
            weights: HybridChannelWeights::default(),
            semantic: Some(false),
        },
        SearchFilters::default(),
        &SemanticRuntimeCredentials::default(),
        &PanicSemanticQueryEmbeddingExecutor,
    )?;

    assert_eq!(output.note.semantic_status, HybridSemanticStatus::Disabled);
    assert!(matches!(
        output.note.lexical_backend,
        Some(SearchLexicalBackend::Ripgrep | SearchLexicalBackend::Mixed)
    ));
    assert_eq!(output.matches.len(), 1);
    assert_eq!(output.matches[0].document.path, "src/runtime.rs");

    cleanup_workspace(&root);
    Ok(())
}

#[test]
fn exact_literal_search_forces_mixed_scrubbed_markdown_and_matches_native_cardinality()
-> FriggResult<()> {
    clear_ripgrep_availability_cache();
    let root = temp_workspace_root("exact-literal-mixed-ripgrep");
    prepare_workspace(
        &root,
        &[
            (
                "docs/readme.md",
                "<!-- needle hidden -->\nneedle markdown\n",
            ),
            ("src/plain.rs", "needle plain\nneedle plain again\n"),
        ],
    )?;
    let query = SearchTextQuery {
        query: "needle".to_owned(),
        path_regex: None,
        limit: 20,
    };
    let mut native_config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    native_config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Native;
    let native = TextSearcher::new(native_config)
        .search_literal_with_execution_options_diagnostics(
            query.clone(),
            SearchFilters::default(),
            SearchTextExecutionOptions::default(),
        )?;

    let fake_rg = write_fake_ripgrep_script(
        &root,
        r#"{"type":"match","data":{"path":{"text":"src/plain.rs"},"lines":{"text":"needle plain\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"needle"},"start":0,"end":6}]}}
{"type":"match","data":{"path":{"text":"src/plain.rs"},"lines":{"text":"needle plain again\n"},"line_number":2,"absolute_offset":13,"submatches":[{"match":{"text":"needle"},"start":0,"end":6}]}}"#,
    )?;
    let mut mixed_config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    mixed_config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Ripgrep;
    mixed_config.lexical_runtime.ripgrep_executable = Some(fake_rg);
    let mixed = TextSearcher::new(mixed_config).search_literal_with_execution_options_diagnostics(
        query,
        SearchFilters::default(),
        SearchTextExecutionOptions::default(),
    )?;

    assert_exact_execution_parity(&native, &mixed);
    assert_eq!(mixed.lexical_backend, Some(SearchLexicalBackend::Mixed));
    assert!(
        mixed
            .lexical_backend_note
            .as_deref()
            .is_some_and(|note| note.contains("native fallback for scrubbed content"))
    );

    cleanup_workspace(&root);
    Ok(())
}

#[cfg(unix)]
#[test]
fn exact_literal_search_falls_back_after_unavailable_or_failed_ripgrep() -> FriggResult<()> {
    use std::os::unix::fs::PermissionsExt;

    clear_ripgrep_availability_cache();
    let root = temp_workspace_root("exact-literal-ripgrep-fallback");
    prepare_workspace(&root, &[("src/lib.rs", "needle\nneedle again\n")])?;
    let query = SearchTextQuery {
        query: "needle".to_owned(),
        path_regex: None,
        limit: 1,
    };
    let mut native_config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    native_config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Native;
    let native = TextSearcher::new(native_config)
        .search_literal_with_execution_options_diagnostics(
            query.clone(),
            SearchFilters::default(),
            SearchTextExecutionOptions::default(),
        )?;

    let missing_rg = root.join("missing-rg");
    let failed_rg = root.join("failed-rg.sh");
    std::fs::write(
        &failed_rg,
        "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo 'ripgrep 15.1.0'; exit 0; fi\necho forced failure >&2\nexit 2\n",
    )
    .map_err(FriggError::Io)?;
    let mut permissions = std::fs::metadata(&failed_rg)
        .map_err(FriggError::Io)?
        .permissions();
    permissions.set_mode(0o755);
    std::fs::set_permissions(&failed_rg, permissions).map_err(FriggError::Io)?;

    for (executable, expected_note) in [
        (missing_rg, "ripgrep unavailable"),
        (failed_rg, "ripgrep execution failed"),
    ] {
        clear_ripgrep_availability_cache();
        let mut config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
        config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Ripgrep;
        config.lexical_runtime.ripgrep_executable = Some(executable);
        let fallback = TextSearcher::new(config)
            .search_literal_with_execution_options_diagnostics(
                query.clone(),
                SearchFilters::default(),
                SearchTextExecutionOptions::default(),
            )?;
        assert_exact_execution_parity(&native, &fallback);
        assert_eq!(fallback.lexical_backend, Some(SearchLexicalBackend::Native));
        assert!(
            fallback
                .lexical_backend_note
                .as_deref()
                .is_some_and(|note| note.contains(expected_note))
        );
    }

    cleanup_workspace(&root);
    Ok(())
}

#[test]
fn exact_regex_search_matches_forced_native_and_ripgrep_backends() -> FriggResult<()> {
    clear_ripgrep_availability_cache();
    let root = temp_workspace_root("exact-regex-ripgrep-parity");
    prepare_workspace(
        &root,
        &[
            ("src/a.rs", "needle 10\n"),
            ("src/b.rs", "needle 20\nneedle 30\n"),
        ],
    )?;
    let query = SearchTextQuery {
        query: r"needle\s+\d+".to_owned(),
        path_regex: Some(Regex::new(r"^src/").expect("valid fixture regex")),
        limit: 20,
    };
    let mut native_config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    native_config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Native;
    let native = TextSearcher::new(native_config).search_regex_with_execution_options_diagnostics(
        query.clone(),
        SearchFilters::default(),
        SearchTextExecutionOptions::default(),
    )?;

    let fake_rg = write_fake_ripgrep_script(
        &root,
        r#"{"type":"match","data":{"path":{"text":"src/a.rs"},"lines":{"text":"needle 10\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"needle 10"},"start":0,"end":9}]}}
{"type":"match","data":{"path":{"text":"src/b.rs"},"lines":{"text":"needle 20\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"needle 20"},"start":0,"end":9}]}}
{"type":"match","data":{"path":{"text":"src/b.rs"},"lines":{"text":"needle 30\n"},"line_number":2,"absolute_offset":10,"submatches":[{"match":{"text":"needle 30"},"start":0,"end":9}]}}"#,
    )?;
    let mut ripgrep_config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    ripgrep_config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Ripgrep;
    ripgrep_config.lexical_runtime.ripgrep_executable = Some(fake_rg);
    let ripgrep = TextSearcher::new(ripgrep_config)
        .search_regex_with_execution_options_diagnostics(
            query,
            SearchFilters::default(),
            SearchTextExecutionOptions::default(),
        )?;

    assert_exact_execution_parity(&native, &ripgrep);
    assert_eq!(ripgrep.lexical_backend, Some(SearchLexicalBackend::Ripgrep));

    cleanup_workspace(&root);
    Ok(())
}

#[test]
fn exact_regex_search_matches_forced_native_ripgrep_and_mixed_backends() -> FriggResult<()> {
    clear_ripgrep_availability_cache();
    let root = temp_workspace_root("exact-regex-backend-parity");
    prepare_workspace(
        &root,
        &[
            ("src/a.rs", "needle 10\n"),
            ("src/b.rs", "needle 20\nneedle 30\n"),
            ("docs/readme.md", "<!-- needle 99 -->\nneedle 40\n"),
        ],
    )?;
    let query = SearchTextQuery {
        query: r"needle\s+\d+".to_owned(),
        path_regex: Some(Regex::new(r"^(?:src/|docs/)").expect("valid fixture regex")),
        limit: 20,
    };
    let mut native_config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    native_config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Native;
    let native = TextSearcher::new(native_config).search_regex_with_execution_options_diagnostics(
        query.clone(),
        SearchFilters::default(),
        SearchTextExecutionOptions::default(),
    )?;

    let fake_rg = write_fake_ripgrep_script(
        &root,
        r#"{"type":"match","data":{"path":{"text":"src/a.rs"},"lines":{"text":"needle 10\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"needle 10"},"start":0,"end":9}]}}
{"type":"match","data":{"path":{"text":"src/b.rs"},"lines":{"text":"needle 20\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"needle 20"},"start":0,"end":9}]}}
{"type":"match","data":{"path":{"text":"src/b.rs"},"lines":{"text":"needle 30\n"},"line_number":2,"absolute_offset":10,"submatches":[{"match":{"text":"needle 30"},"start":0,"end":9}]}}"#,
    )?;
    let mut mixed_config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    mixed_config.lexical_runtime.backend = crate::settings::LexicalBackendMode::Ripgrep;
    mixed_config.lexical_runtime.ripgrep_executable = Some(fake_rg);
    let mixed = TextSearcher::new(mixed_config).search_regex_with_execution_options_diagnostics(
        query,
        SearchFilters::default(),
        SearchTextExecutionOptions::default(),
    )?;

    assert_exact_execution_parity(&native, &mixed);
    assert_eq!(mixed.lexical_backend, Some(SearchLexicalBackend::Mixed));

    cleanup_workspace(&root);
    Ok(())
}