oy-cli 0.10.7

Local AI coding CLI for inspecting, editing, running commands, and auditing repositories
Documentation
use super::*;
#[test]
fn search_accepts_space_separated_paths() {
    let (dir, ctx) = test_context(auto_policy(), false);
    fs::create_dir(dir.path().join("src")).unwrap();
    fs::write(dir.path().join("src/app.rs"), "fn app_hit() {}\n").unwrap();
    fs::write(dir.path().join("src/ui.rs"), "fn ui_hit() {}\n").unwrap();
    fs::write(dir.path().join("src/other.rs"), "fn other_hit() {}\n").unwrap();

    let value = workspace::tool_search(
        &ctx,
        SearchArgs {
            pattern: "fn (app|ui)_hit".into(),
            path: "src/app.rs src/ui.rs".into(),
            exclude: None,
            limit: 10,
            mode: SearchMode::Regex,
        },
    )
    .unwrap();

    assert_eq!(value["match_count"], 2);
    let paths = value["matches"]
        .as_array()
        .unwrap()
        .iter()
        .map(|item| item["path"].as_str().unwrap().to_string())
        .collect::<Vec<_>>();
    assert!(paths.iter().any(|path| path == "src/app.rs"));
    assert!(paths.iter().any(|path| path == "src/ui.rs"));
    assert!(!paths.iter().any(|path| path == "src/other.rs"));
}

#[test]
fn search_auto_falls_back_to_literal_for_invalid_regex() {
    let (dir, ctx) = test_context(auto_policy(), false);
    fs::write(dir.path().join("notes.txt"), "literal [text\n").unwrap();

    let value = workspace::tool_search(
        &ctx,
        SearchArgs {
            pattern: "[text".into(),
            path: "notes.txt".into(),
            exclude: None,
            limit: 10,
            mode: SearchMode::Auto,
        },
    )
    .unwrap();

    assert_eq!(value["mode"], "literal");
    assert_eq!(value["match_count"], 1);
    assert!(
        value["warning"]
            .as_str()
            .unwrap()
            .contains("searched literally")
    );
}

#[test]
fn search_auto_treats_plain_identifier_as_literal_and_suggests_read_path() {
    let (dir, ctx) = test_context(auto_policy(), false);
    fs::write(dir.path().join("a.txt"), "foo.bar\nfooXbar\n").unwrap();
    fs::write(dir.path().join("b.txt"), "foo.bar\n").unwrap();

    let value = workspace::tool_search(
        &ctx,
        SearchArgs {
            pattern: "foo.bar".into(),
            path: ".".into(),
            exclude: None,
            limit: 10,
            mode: SearchMode::Auto,
        },
    )
    .unwrap();

    assert_eq!(value["mode"], "regex");
    assert_eq!(value["match_count"], 3);
    assert_eq!(value["read_path"], "a.txt");
    assert_eq!(value["file_count"], 2);

    let value = workspace::tool_search(
        &ctx,
        SearchArgs {
            pattern: "foo_bar".into(),
            path: ".".into(),
            exclude: None,
            limit: 10,
            mode: SearchMode::Auto,
        },
    )
    .unwrap();

    assert_eq!(value["mode"], "literal");
    assert_eq!(value["match_count"], 0);
}

#[test]
fn directory_exclude_applies_to_search_and_replace_file_targets() {
    let (dir, ctx) = test_context(auto_policy(), false);
    fs::create_dir(dir.path().join("generated")).unwrap();
    fs::write(dir.path().join("generated/a.txt"), "hit\n").unwrap();
    fs::write(dir.path().join("keep.txt"), "hit\n").unwrap();

    let value = workspace::tool_search(
        &ctx,
        SearchArgs {
            pattern: "hit".into(),
            path: ".".into(),
            exclude: Some(ExcludeArg::String("generated/".into())),
            limit: 10,
            mode: SearchMode::Literal,
        },
    )
    .unwrap();
    assert_eq!(value["match_count"], 1);
    assert_eq!(value["matches"][0]["path"], "keep.txt");

    let value = workspace::tool_replace(
        &ctx,
        ReplaceArgs {
            pattern: "hit".into(),
            replacement: "miss".into(),
            path: "generated/a.txt".into(),
            exclude: Some(ExcludeArg::String("generated/".into())),
            limit: 10,
            mode: ReplaceMode::Literal,
        },
    )
    .unwrap();
    assert_eq!(value["replacement_count"], 0);
    assert_eq!(
        fs::read_to_string(dir.path().join("generated/a.txt")).unwrap(),
        "hit\n"
    );
}

#[test]
fn search_stops_at_requested_limit() {
    let (dir, ctx) = test_context(auto_policy(), false);
    fs::write(dir.path().join("hits.txt"), "hit\nhit\nhit\n").unwrap();
    let value = workspace::tool_search(
        &ctx,
        SearchArgs {
            pattern: "hit".into(),
            path: "hits.txt".into(),
            exclude: None,
            limit: 2,
            mode: SearchMode::Literal,
        },
    )
    .unwrap();
    assert_eq!(value["match_count"], 2);
    assert_eq!(value["matches"].as_array().unwrap().len(), 2);
    assert_eq!(value["truncated"], true);
}

#[test]
fn search_exact_file_does_not_spend_limit_on_siblings() {
    let (dir, ctx) = test_context(auto_policy(), false);
    fs::create_dir(dir.path().join("src")).unwrap();
    fs::write(dir.path().join("src/aaa.rs"), "hit\n".repeat(10_050)).unwrap();
    fs::write(dir.path().join("src/target.rs"), "hit\nhit\n").unwrap();

    let value = workspace::tool_search(
        &ctx,
        SearchArgs {
            pattern: "hit".into(),
            path: "src/target.rs".into(),
            exclude: None,
            limit: 2,
            mode: SearchMode::Literal,
        },
    )
    .unwrap();

    assert_eq!(value["match_count"], 2);
    assert_eq!(value["matches"][0]["path"], "src/target.rs");
    assert_eq!(value["matches"][1]["path"], "src/target.rs");
    assert_eq!(value["truncated"], false);
}

#[test]
fn search_file_treats_zip_as_binary_file() {
    let (dir, ctx) = test_context(auto_policy(), false);
    fs::write(dir.path().join("sample.zip"), b"PK\0\0not searched").unwrap();
    let value = workspace::tool_search(
        &ctx,
        SearchArgs {
            pattern: "not searched".into(),
            path: "sample.zip".into(),
            exclude: None,
            limit: 10,
            mode: SearchMode::Literal,
        },
    )
    .unwrap();
    assert_eq!(value["match_count"], 0);
}