frigg 0.3.2

Local-first MCP server for code understanding.
Documentation
use super::*;

#[test]
fn ordering_literal_search_repeated_runs_are_identical() -> FriggResult<()> {
    let root_a = temp_workspace_root("ordering-literal-a");
    let root_b = temp_workspace_root("ordering-literal-b");
    prepare_workspace(
        &root_a,
        &[("z.txt", "needle z\n"), ("a.txt", "x needle\ny needle\n")],
    )?;
    prepare_workspace(&root_b, &[("b.txt", "needle b\n")])?;

    let config = FriggConfig::from_workspace_roots(vec![root_b.clone(), root_a.clone()])?;
    let searcher = TextSearcher::new(config);
    let query = SearchTextQuery {
        query: "needle".to_owned(),
        path_regex: None,
        limit: 100,
    };

    let first = searcher.search_literal_with_filters(query.clone(), SearchFilters::default())?;
    let second = searcher.search_literal_with_filters(query, SearchFilters::default())?;
    assert_eq!(first, second);

    cleanup_workspace(&root_a);
    cleanup_workspace(&root_b);
    Ok(())
}

#[test]
fn ordering_regex_search_repeated_runs_are_identical() -> FriggResult<()> {
    let root = temp_workspace_root("ordering-regex");
    prepare_workspace(
        &root,
        &[
            ("src/lib.rs", "needle 1\nneedle 2\n"),
            ("README.md", "needle docs\n"),
        ],
    )?;

    let config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
    let searcher = TextSearcher::new(config);
    let query = SearchTextQuery {
        query: r"needle\s+\d+".to_owned(),
        path_regex: None,
        limit: 100,
    };

    let first = searcher.search_regex_with_filters(query.clone(), SearchFilters::default())?;
    let second = searcher.search_regex_with_filters(query, SearchFilters::default())?;
    assert_eq!(first, second);

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

#[test]
fn ordering_filter_normalization_applies_repo_path_and_language() -> FriggResult<()> {
    let root_a = temp_workspace_root("ordering-filters-a");
    let root_b = temp_workspace_root("ordering-filters-b");
    prepare_workspace(
        &root_a,
        &[
            ("src/lib.rs", "needle 1\n"),
            ("src/lib.php", "needle 2\n"),
            ("src/lib.tsx", "needle 3\n"),
            ("src/lib.py", "needle 4\n"),
            ("src/lib.go", "needle 5\n"),
            ("src/lib.kts", "needle 6\n"),
            ("src/lib.java", "needle 19\n"),
            ("src/lib.lua", "needle 7\n"),
            ("src/lib.roc", "needle 8\n"),
            ("src/lib.nims", "needle 14\n"),
        ],
    )?;
    prepare_workspace(
        &root_b,
        &[
            ("src/main.rs", "needle 9\n"),
            ("src/main.php", "needle 10\n"),
            ("src/main.ts", "needle 11\n"),
            ("src/main.py", "needle 12\n"),
            ("src/main.go", "needle 13\n"),
            ("src/main.kt", "needle 15\n"),
            ("src/main.java", "needle 20\n"),
            ("src/main.lua", "needle 16\n"),
            ("src/main.roc", "needle 17\n"),
            ("src/main.nim", "needle 18\n"),
        ],
    )?;

    let config = FriggConfig::from_workspace_roots(vec![root_a.clone(), root_b.clone()])?;
    let searcher = TextSearcher::new(config);
    let query =
        SearchTextQuery {
            query: r"needle\s+\d+".to_owned(),
            path_regex: Some(Regex::new(r"^src/.*$").map_err(|err| {
                FriggError::InvalidInput(format!("invalid test path regex: {err}"))
            })?),
            limit: 100,
        };

    let matches = searcher.search_regex_with_filters(
        query,
        SearchFilters {
            repository_id: Some("  repo-002  ".to_owned()),
            language: Some("  RS ".to_owned()),
        },
    )?;
    assert_eq!(
        matches,
        vec![text_match("repo-002", "src/main.rs", 1, 1, "needle 9")]
    );

    let typescript_matches = searcher.search_regex_with_filters(
        SearchTextQuery {
            query: r"needle".to_owned(),
            path_regex: None,
            limit: 10,
        },
        SearchFilters {
            repository_id: None,
            language: Some("tsx".to_owned()),
        },
    )?;
    assert_eq!(
        typescript_matches,
        vec![
            text_match("repo-001", "src/lib.tsx", 1, 1, "needle 3"),
            text_match("repo-002", "src/main.ts", 1, 1, "needle 11"),
        ]
    );

    let python_matches = searcher.search_regex_with_filters(
        SearchTextQuery {
            query: r"needle".to_owned(),
            path_regex: None,
            limit: 10,
        },
        SearchFilters {
            repository_id: None,
            language: Some("py".to_owned()),
        },
    )?;
    assert_eq!(
        python_matches,
        vec![
            text_match("repo-001", "src/lib.py", 1, 1, "needle 4"),
            text_match("repo-002", "src/main.py", 1, 1, "needle 12"),
        ]
    );

    let go_matches = searcher.search_regex_with_filters(
        SearchTextQuery {
            query: r"needle".to_owned(),
            path_regex: None,
            limit: 10,
        },
        SearchFilters {
            repository_id: None,
            language: Some("golang".to_owned()),
        },
    )?;
    assert_eq!(
        go_matches,
        vec![
            text_match("repo-001", "src/lib.go", 1, 1, "needle 5"),
            text_match("repo-002", "src/main.go", 1, 1, "needle 13"),
        ]
    );

    let kotlin_matches = searcher.search_regex_with_filters(
        SearchTextQuery {
            query: r"needle".to_owned(),
            path_regex: None,
            limit: 10,
        },
        SearchFilters {
            repository_id: None,
            language: Some("kt".to_owned()),
        },
    )?;
    assert_eq!(
        kotlin_matches,
        vec![
            text_match("repo-001", "src/lib.kts", 1, 1, "needle 6"),
            text_match("repo-002", "src/main.kt", 1, 1, "needle 15"),
        ]
    );

    let java_matches = searcher.search_regex_with_filters(
        SearchTextQuery {
            query: r"needle".to_owned(),
            path_regex: None,
            limit: 10,
        },
        SearchFilters {
            repository_id: None,
            language: Some("java".to_owned()),
        },
    )?;
    assert_eq!(
        java_matches,
        vec![
            text_match("repo-001", "src/lib.java", 1, 1, "needle 19"),
            text_match("repo-002", "src/main.java", 1, 1, "needle 20"),
        ]
    );

    let lua_matches = searcher.search_regex_with_filters(
        SearchTextQuery {
            query: r"needle".to_owned(),
            path_regex: None,
            limit: 10,
        },
        SearchFilters {
            repository_id: None,
            language: Some("lua".to_owned()),
        },
    )?;
    assert_eq!(
        lua_matches,
        vec![
            text_match("repo-001", "src/lib.lua", 1, 1, "needle 7"),
            text_match("repo-002", "src/main.lua", 1, 1, "needle 16"),
        ]
    );

    let roc_matches = searcher.search_regex_with_filters(
        SearchTextQuery {
            query: r"needle".to_owned(),
            path_regex: None,
            limit: 10,
        },
        SearchFilters {
            repository_id: None,
            language: Some("roc".to_owned()),
        },
    )?;
    assert_eq!(
        roc_matches,
        vec![
            text_match("repo-001", "src/lib.roc", 1, 1, "needle 8"),
            text_match("repo-002", "src/main.roc", 1, 1, "needle 17"),
        ]
    );

    let nim_matches = searcher.search_regex_with_filters(
        SearchTextQuery {
            query: r"needle".to_owned(),
            path_regex: None,
            limit: 10,
        },
        SearchFilters {
            repository_id: None,
            language: Some("nim".to_owned()),
        },
    )?;
    assert_eq!(
        nim_matches,
        vec![
            text_match("repo-001", "src/lib.nims", 1, 1, "needle 14"),
            text_match("repo-002", "src/main.nim", 1, 1, "needle 18"),
        ]
    );

    let unsupported_language = searcher.search_regex_with_filters(
        SearchTextQuery {
            query: r"needle".to_owned(),
            path_regex: None,
            limit: 10,
        },
        SearchFilters {
            repository_id: None,
            language: Some("javascript".to_owned()),
        },
    );
    let err = unsupported_language.expect_err("unsupported language filter should fail");
    assert!(
        err.to_string().contains("unsupported language filter"),
        "unexpected unsupported-language error: {err}"
    );

    cleanup_workspace(&root_a);
    cleanup_workspace(&root_b);
    Ok(())
}