srcwalk 0.1.9

Tree-sitter indexed lookups — smart code reading for AI agents
Documentation
use std::process::Command;

fn srcwalk() -> Command {
    Command::new(env!("CARGO_BIN_EXE_srcwalk"))
}

fn assert_tips_are_trailing(stdout: &str) {
    let trimmed = stdout.trim_end();
    let last_tip = trimmed
        .rfind("> Tip:")
        .unwrap_or_else(|| panic!("expected at least one footer tip:\n{stdout}"));
    assert!(
        trimmed[last_tip..]
            .lines()
            .all(|line| line.starts_with("> Tip:") || line.is_empty()),
        "tips should be trailing footer lines, got:\n{stdout}"
    );
}

#[test]
fn symbol_search_exposes_class_kind_range_and_child_context() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("DependencyProperty.cs"),
        r#"namespace Microsoft.UI.Xaml
{
    public partial class DependencyProperty
    {
        public DependencyProperty()
        {
        }
    }
}
"#,
    )
    .unwrap();

    let out = srcwalk()
        .args(["DependencyProperty", "--glob", "*.cs", "--scope"])
        .arg(dir.path())
        .output()
        .unwrap();
    let stdout = String::from_utf8_lossy(&out.stdout);

    assert!(
        out.status.success(),
        "symbol search should succeed, stderr:\n{}\nstdout:\n{stdout}",
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(
        stdout.contains("class DependencyProperty") && stdout.contains("3-8"),
        "expected class kind/range semantic context, got:\n{stdout}"
    );
    assert!(
        stdout.contains("fn DependencyProperty") && stdout.contains("5-7"),
        "expected constructor/function child semantic context, got:\n{stdout}"
    );
    assert!(
        stdout.contains("Microsoft.UI.Xaml"),
        "expected namespace/module context, got:\n{stdout}"
    );
}

#[test]
fn symbol_search_pagination_tip_remains_trailing_with_semantic_context() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("DependencyProperty.cs"),
        r#"namespace Microsoft.UI.Xaml
{
    public partial class DependencyProperty
    {
        public DependencyProperty()
        {
        }
    }
}
"#,
    )
    .unwrap();

    let out = srcwalk()
        .args([
            "DependencyProperty",
            "--glob",
            "*.cs",
            "--limit",
            "1",
            "--scope",
        ])
        .arg(dir.path())
        .output()
        .unwrap();
    let stdout = String::from_utf8_lossy(&out.stdout);

    assert!(
        stdout.contains("class DependencyProperty"),
        "pagination should not remove semantic class context, got:\n{stdout}"
    );
    assert!(
        stdout.contains("--offset 1 --limit 1"),
        "expected actionable pagination tip, got:\n{stdout}"
    );
    assert_tips_are_trailing(&stdout);
}

#[test]
fn symbol_search_facets_use_semantic_compact_definition_rows() {
    let dir = tempfile::tempdir().unwrap();
    for idx in 0..6 {
        std::fs::write(
            dir.path().join(format!("DependencyProperty{idx}.cs")),
            format!(
                r#"namespace Microsoft.UI.Xaml
{{
    public partial class DependencyProperty
    {{
        public DependencyProperty()
        {{
        }}
    }}
}}
"#
            ),
        )
        .unwrap();
    }

    let out = srcwalk()
        .args(["DependencyProperty", "--glob", "*.cs", "--scope"])
        .arg(dir.path())
        .output()
        .unwrap();
    let stdout = String::from_utf8_lossy(&out.stdout);

    assert!(
        stdout.contains("### Definitions (6)"),
        "expected faceted definitions output, got:\n{stdout}"
    );
    assert!(
        stdout.contains("[class] Microsoft.UI.Xaml.DependencyProperty"),
        "expected semantic compact class rows with namespace context, got:\n{stdout}"
    );
    assert!(
        stdout.contains("+[fn] DependencyProperty"),
        "expected child function breadcrumbs, got:\n{stdout}"
    );
    assert!(
        !stdout.contains("### File overview:"),
        "compact facets should avoid duplicate basename file overview, got:\n{stdout}"
    );
}

#[test]
fn symbol_search_semantic_rows_work_across_rust_typescript_and_python() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("widget.rs"),
        "pub struct Widget {}\nimpl Widget { pub fn build(&self) {} }\n",
    )
    .unwrap();
    std::fs::write(
        dir.path().join("widget.ts"),
        "export class Widget {\n  build(arg: string) { return arg; }\n}\n",
    )
    .unwrap();
    std::fs::write(
        dir.path().join("widget.py"),
        "class Widget:\n    def build(self):\n        return 1\n",
    )
    .unwrap();

    let rust = srcwalk()
        .args(["Widget", "--glob", "*.rs", "--scope"])
        .arg(dir.path())
        .output()
        .unwrap();
    let rust_stdout = String::from_utf8_lossy(&rust.stdout);
    assert!(
        rust_stdout.contains("[struct] Widget") || rust_stdout.contains("struct Widget"),
        "expected Rust struct semantic row/context, got:\n{rust_stdout}"
    );

    let typescript = srcwalk()
        .args(["Widget", "--glob", "*.ts", "--scope"])
        .arg(dir.path())
        .output()
        .unwrap();
    let typescript_stdout = String::from_utf8_lossy(&typescript.stdout);
    assert!(
        typescript_stdout.contains("[class] Widget"),
        "expected TypeScript exported class semantic row, got:\n{typescript_stdout}"
    );
    assert!(
        !typescript_stdout.contains("[export] export class Widget"),
        "TypeScript exported class should not fall back to generic export row, got:\n{typescript_stdout}"
    );

    let python = srcwalk()
        .args(["Widget", "--glob", "*.py", "--scope"])
        .arg(dir.path())
        .output()
        .unwrap();
    let python_stdout = String::from_utf8_lossy(&python.stdout);
    assert!(
        python_stdout.contains("[class] Widget") || python_stdout.contains("class Widget"),
        "expected Python class semantic row/context, got:\n{python_stdout}"
    );
    assert!(
        python_stdout.contains("+[fn] build") || python_stdout.contains("fn build"),
        "expected Python child function semantic breadcrumb/context, got:\n{python_stdout}"
    );
}

#[test]
fn callers_semantic_rows_work_across_go_python_and_rust() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("caller.go"),
        r#"package main

func caller() {
    sdktranslator.TranslateRequest(from, to, model, rawJSON, stream)
}
"#,
    )
    .unwrap();
    std::fs::write(
        dir.path().join("caller.py"),
        "def py_caller(client):\n    return client.translate_request(payload, stream)\n",
    )
    .unwrap();
    std::fs::write(
        dir.path().join("caller.rs"),
        "fn rust_caller(client: Client) { client.translate_request(payload, stream); }\n",
    )
    .unwrap();

    let go = srcwalk()
        .args(["TranslateRequest", "--callers", "--glob", "*.go", "--scope"])
        .arg(dir.path())
        .output()
        .unwrap();
    let go_stdout = String::from_utf8_lossy(&go.stdout);
    assert!(
        go_stdout.contains("[fn] caller")
            && go_stdout.contains("recv=sdktranslator")
            && go_stdout.contains("args=5"),
        "expected Go caller semantic row, got:\n{go_stdout}"
    );

    let python = srcwalk()
        .args([
            "translate_request",
            "--callers",
            "--glob",
            "*.py",
            "--scope",
        ])
        .arg(dir.path())
        .output()
        .unwrap();
    let python_stdout = String::from_utf8_lossy(&python.stdout);
    assert!(
        python_stdout.contains("[fn] py_caller")
            && python_stdout.contains("recv=client")
            && python_stdout.contains("args=2"),
        "expected Python caller semantic row, got:\n{python_stdout}"
    );

    let rust = srcwalk()
        .args([
            "translate_request",
            "--callers",
            "--glob",
            "*.rs",
            "--scope",
        ])
        .arg(dir.path())
        .output()
        .unwrap();
    let rust_stdout = String::from_utf8_lossy(&rust.stdout);
    assert!(
        rust_stdout.contains("[fn] rust_caller")
            && rust_stdout.contains("recv=client")
            && rust_stdout.contains("args=2"),
        "expected Rust caller semantic row, got:\n{rust_stdout}"
    );
}

#[test]
fn callers_output_preserves_scope_receiver_args_and_omits_call_text_by_default() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("caller.go"),
        r#"package main

func caller() {
    sdktranslator.TranslateRequest(from, to, model, rawJSON, stream)
}

func other() {
    TranslateRequest(from, to, model, rawJSON, stream)
}
"#,
    )
    .unwrap();

    let out = srcwalk()
        .args(["TranslateRequest", "--callers", "--limit", "1", "--scope"])
        .arg(dir.path())
        .output()
        .unwrap();
    let stdout = String::from_utf8_lossy(&out.stdout);

    assert!(
        out.status.success(),
        "callers search should succeed, stderr:\n{}\nstdout:\n{stdout}",
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(
        stdout.contains("caller: caller") || stdout.contains("[fn] caller"),
        "expected caller function scope, got:\n{stdout}"
    );
    assert!(
        stdout.contains("receiver: sdktranslator") || stdout.contains("recv=sdktranslator"),
        "expected receiver metadata, got:\n{stdout}"
    );
    assert!(
        stdout.contains("args: 5") || stdout.contains("args=5"),
        "expected argument count metadata, got:\n{stdout}"
    );
    assert!(
        !stdout.contains("sdktranslator.TranslateRequest(from, to, model, rawJSON, stream)"),
        "default caller output should omit call text; use --expand for source context, got:\n{stdout}"
    );
    assert!(
        stdout.contains("--expand[=N]"),
        "expected footer tip to mention --expand, got:\n{stdout}"
    );
    assert!(
        stdout.contains("--offset 1 --limit 1"),
        "expected caller pagination tip, got:\n{stdout}"
    );
    assert_tips_are_trailing(&stdout);
}

#[test]
fn callers_default_is_compact_but_expand_still_shows_source_window() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("caller.go"),
        r#"package main

func caller() {
    sdktranslator.TranslateRequest(from, to, model, rawJSON, stream)
}
"#,
    )
    .unwrap();

    let compact = srcwalk()
        .args(["TranslateRequest", "--callers", "--scope"])
        .arg(dir.path())
        .output()
        .unwrap();
    let compact_stdout = String::from_utf8_lossy(&compact.stdout);
    assert!(
        compact_stdout.contains("<- calls") && compact_stdout.contains("[fn] caller"),
        "expected semantic compact caller row, got:\n{compact_stdout}"
    );
    assert!(
        !compact_stdout
            .contains("sdktranslator.TranslateRequest(from, to, model, rawJSON, stream)"),
        "default caller output should not include call source, got:\n{compact_stdout}"
    );
    assert!(
        !compact_stdout.contains("```"),
        "default caller output should not include source fence, got:\n{compact_stdout}"
    );

    let expanded = srcwalk()
        .args(["TranslateRequest", "--callers", "--expand=1", "--scope"])
        .arg(dir.path())
        .output()
        .unwrap();
    let expanded_stdout = String::from_utf8_lossy(&expanded.stdout);
    assert!(
        expanded_stdout.contains("```")
            && expanded_stdout.contains("")
            && expanded_stdout
                .contains("sdktranslator.TranslateRequest(from, to, model, rawJSON, stream)"),
        "explicit --expand should keep source window with call source, got:\n{expanded_stdout}"
    );
}