gobby-code 1.3.2

Fast Rust CLI for Gobby's code index — AST-aware search, symbol navigation, and dependency graph
Documentation
use super::support::*;
use super::*;

#[test]
fn frontmatter_provenance_accepts_unquoted_and_escaped_values() {
    let files = source_files_from_frontmatter(
        r#"---
provenance:
  - file: src/plain.rs
  - file: "src/escaped\"quote.rs"
---
"#,
    );

    assert!(files.contains("src/plain.rs"));
    assert!(files.contains("src/escaped\"quote.rs"));
}

#[test]
fn frontmatter_provenance_parse_yaml_with_ranges() {
    let files = source_files_from_frontmatter(
        r#"---
title: "Example"
provenance:
  - file: "src/one:thing.rs"
    ranges:
      - "1-4"
  - file: src/two.rs
---
"#,
    );

    assert!(files.contains("src/one:thing.rs"));
    assert!(files.contains("src/two.rs"));
    assert_eq!(files.len(), 2);
}

#[test]
fn frontmatter_legacy_source_files_are_ignored() {
    let files = source_files_from_frontmatter(
        r#"---
source_files:
- file: src/legacy.rs
sources:
- file: src/also-legacy.rs
---
"#,
    );

    assert!(files.is_empty());
}

#[test]
fn source_hashes_reject_frontmatter_paths_outside_project_root() {
    let tempdir = tempfile::tempdir().expect("tempdir");
    let project_root = tempdir.path().join("project");
    std::fs::create_dir_all(&project_root).expect("project root");
    std::fs::write(tempdir.path().join("outside.rs"), "fn outside() {}").expect("outside file");
    let content = r#"---
provenance:
  - file: ../outside.rs
---
"#;

    let err = source_hashes_for_doc(&project_root, content).expect_err("outside source rejected");

    assert!(
        err.to_string().contains("resolves outside project root"),
        "unexpected error: {err}"
    );
}

#[test]
fn yaml_unquote_translates_common_escapes_and_rejects_incomplete_escape() {
    assert_eq!(
        unquote_yaml_string(r#""line\nquote\"tab\tbackslash\\""#),
        Some("line\nquote\"tab\tbackslash\\".to_string())
    );
    assert_eq!(
        unquote_yaml_string(r#""hex\x21 unicode\u2713 scalar\U0001F680""#),
        Some("hex! unicode\u{2713} scalar\u{1f680}".to_string())
    );
    let incomplete = format!("\"{}\\\"", "src/incomplete");
    assert_eq!(unquote_yaml_string(&incomplete), None);
    assert_eq!(unquote_yaml_string(r#""bad\x1""#), None);
    assert_eq!(unquote_yaml_string(r#""bad\u12xz""#), None);
    assert_eq!(unquote_yaml_string(r#""bad\U00110000""#), None);
}

#[test]
fn frontmatter_serializes_scalars_with_serde_yaml() {
    let source_file = "src/quote\"colon:thing.rs";
    let doc = frontmatter(
        "line\nquote\"tab\tbackslash\\nul\0bell\u{0007}",
        "code_file",
        &[SourceSpan {
            file: source_file.to_string(),
            line_start: 7,
            line_end: 9,
        }],
    );
    let yaml = doc
        .strip_prefix("---\n")
        .and_then(|content| content.strip_suffix("---\n\n"))
        .expect("frontmatter delimiters");
    let parsed: serde_yaml::Value = serde_yaml::from_str(yaml).expect("frontmatter parses");
    let serde_yaml::Value::Mapping(mapping) = parsed else {
        panic!("frontmatter is a YAML mapping");
    };

    assert_eq!(
        mapping
            .get(serde_yaml::Value::String("title".to_string()))
            .and_then(serde_yaml::Value::as_str),
        Some("line\nquote\"tab\tbackslash\\nul\0bell\u{0007}")
    );
    assert_eq!(
        mapping
            .get(serde_yaml::Value::String("type".to_string()))
            .and_then(serde_yaml::Value::as_str),
        Some("code_file")
    );
    assert!(source_files_from_frontmatter(&doc).contains(source_file));
}

#[test]
fn relevant_source_files_header_links_to_coalesced_line_ranges() {
    let mut doc = String::new();
    append_relevant_source_files(
        &mut doc,
        &[
            SourceSpan {
                file: "src/space file[1].rs".to_string(),
                line_start: 7,
                line_end: 9,
            },
            SourceSpan {
                file: "src/space file[1].rs".to_string(),
                line_start: 10,
                line_end: 10,
            },
        ],
    );

    assert!(doc.starts_with("<details>\n<summary>Relevant source files</summary>"));
    assert!(doc.contains("- [src/space file\\[1\\].rs:7-10](src/space%20file%5B1%5D.rs#L7-L10)"));
    assert!(doc.ends_with("</details>\n\n"));
}

#[test]
fn frontmatter_matches_the_shared_codewiki_contract_golden() {
    let doc = frontmatter_with_degradation(
        "src/lib.rs",
        "file",
        &[
            SourceSpan {
                file: "src/lib.rs".to_string(),
                line_start: 1,
                line_end: 2,
            },
            SourceSpan {
                file: "src/lib.rs".to_string(),
                line_start: 10,
                line_end: 10,
            },
        ],
        &["model_provider_unavailable".to_string()],
    );

    assert!(
        gobby_core::codewiki_contract::GOLDEN_PAGE.starts_with(&doc),
        "emitted frontmatter drifted from gobby_core::codewiki_contract::GOLDEN_PAGE:\n{doc}"
    );
}

#[test]
fn citations_validated_against_spans() {
    let input = CodewikiInput {
        leading_chunks: std::collections::BTreeMap::new(),
        files: vec!["src/lib.rs".to_string()],
        graph_edges: Vec::new(),
        graph_availability: CodewikiGraphAvailability::Available,
        symbols: vec![
            test_symbol_range(
                "src/lib.rs",
                "Client",
                "class",
                10,
                14,
                "pub struct Client {",
            ),
            test_symbol_range(
                "src/lib.rs",
                "connect",
                "function",
                20,
                24,
                "pub fn connect()",
            ),
        ],
    };
    let mut generator = |prompt: &str, _system: &str, _tier: PromptTier| {
        if prompt.contains("Client") {
            Some("Builds client state [src/lib.rs:999].".to_string())
        } else if prompt.contains("connect") {
            Some("Opens a connection [src/lib.rs:20].".to_string())
        } else {
            Some("Coordinates the public API [missing.rs:1].".to_string())
        }
    };

    let docs = generate_hierarchical_docs(&input, Some(&mut generator));
    let file_doc = docs
        .iter()
        .find(|(path, _)| path == "code/files/src/lib.rs.md")
        .map(|(_, content)| content)
        .expect("file doc");

    assert!(!file_doc.contains("source:\n"));
    assert!(file_doc.contains("provenance:\n"));
    assert!(source_files_from_frontmatter(file_doc).contains("src/lib.rs"));
    assert!(file_doc.contains("10-14"));
    assert!(file_doc.contains("20-24"));
    assert!(file_doc.contains("[src/lib.rs:10-14]"));
    assert!(file_doc.contains("[src/lib.rs:20]"));
    assert!(!file_doc.contains("src/lib.rs:999"));
    assert!(!file_doc.contains("missing.rs:1"));
}