collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
#[cfg(test)]
#[allow(clippy::module_inception)]
mod tests {
    use std::collections::HashMap;
    use std::sync::Arc;

    use crate::lsp::protocol::{DocumentSymbol, LspPosition, LspRange, SymbolKind};
    use crate::repo_map::parser;

    use super::super::{
        DiagnosticsCache,
        convert::{path_to_uri, to_repo_symbols, uri_to_path},
        server_config::{
            extension_to_language_id, find_server_for_language, is_binary_available, known_servers,
        },
    };

    #[test]
    fn test_path_to_uri() {
        assert_eq!(
            path_to_uri("/home/user/project"),
            "file:///home/user/project"
        );
        assert_eq!(path_to_uri("/tmp/a.rs"), "file:///tmp/a.rs");
        // Relative / Windows-style gets a triple-slash.
        assert_eq!(path_to_uri("C:/Users/me"), "file:///C:/Users/me");
    }

    #[test]
    fn test_uri_to_path() {
        assert_eq!(
            uri_to_path("file:///home/user/project"),
            "/home/user/project"
        );
        assert_eq!(uri_to_path("file:///tmp/a.rs"), "/tmp/a.rs");
        // Windows round-trip.
        assert_eq!(uri_to_path("file:///C:/Users/me"), "C:/Users/me");
        // Non file:// URI returned as-is.
        assert_eq!(uri_to_path("https://example.com"), "https://example.com");
    }

    #[test]
    fn test_known_servers_not_empty() {
        let servers = known_servers();
        assert!(
            servers.len() >= 48,
            "Expected at least 48 known servers, got {}",
            servers.len()
        );

        // Verify key servers are in the list.
        let commands: Vec<&str> = servers.iter().map(|s| s.command.as_str()).collect();
        assert!(commands.contains(&"rust-analyzer"));
        assert!(commands.contains(&"gopls"));
        assert!(commands.contains(&"clangd"));
        assert!(commands.contains(&"jdtls"));
        assert!(commands.contains(&"solargraph"));

        // Each server should have a non-empty language_id.
        for s in &servers {
            assert!(!s.language_id.is_empty());
            assert!(!s.command.is_empty());
        }
    }

    #[test]
    fn test_extension_to_language_id() {
        assert_eq!(extension_to_language_id("rs"), Some("rust"));
        assert_eq!(extension_to_language_id("py"), Some("python"));
        assert_eq!(extension_to_language_id("ts"), Some("typescript"));
        assert_eq!(extension_to_language_id("tsx"), Some("typescript"));
        assert_eq!(extension_to_language_id("js"), Some("javascript"));
        assert_eq!(extension_to_language_id("go"), Some("go"));
        assert_eq!(extension_to_language_id("java"), Some("java"));
        assert_eq!(extension_to_language_id("c"), Some("c"));
        assert_eq!(extension_to_language_id("cpp"), Some("cpp"));
        assert_eq!(extension_to_language_id("cs"), Some("csharp"));
        assert_eq!(extension_to_language_id("rb"), Some("ruby"));
        assert_eq!(extension_to_language_id("php"), Some("php"));
        assert_eq!(extension_to_language_id("swift"), Some("swift"));
        assert_eq!(extension_to_language_id("kt"), Some("kotlin"));
        assert_eq!(extension_to_language_id("lua"), Some("lua"));
        assert_eq!(extension_to_language_id("zig"), Some("zig"));
        assert_eq!(extension_to_language_id("ex"), Some("elixir"));
        assert_eq!(extension_to_language_id("dart"), Some("dart"));
        assert_eq!(extension_to_language_id("sh"), Some("shellscript"));
        assert_eq!(extension_to_language_id("vue"), Some("vue"));
        assert_eq!(extension_to_language_id("svelte"), Some("svelte"));
        assert_eq!(extension_to_language_id("html"), Some("html"));
        assert_eq!(extension_to_language_id("css"), Some("css"));
        assert_eq!(extension_to_language_id("scss"), Some("scss"));
        assert_eq!(extension_to_language_id("json"), Some("json"));
        assert_eq!(extension_to_language_id("toml"), Some("toml"));
        assert_eq!(extension_to_language_id("hs"), Some("haskell"));
        assert_eq!(extension_to_language_id("ml"), Some("ocaml"));
        assert_eq!(extension_to_language_id("fs"), Some("fsharp"));
        assert_eq!(extension_to_language_id("clj"), Some("clojure"));
        assert_eq!(extension_to_language_id("erl"), Some("erlang"));
        assert_eq!(extension_to_language_id("nim"), Some("nim"));
        assert_eq!(extension_to_language_id("jl"), Some("julia"));
        assert_eq!(extension_to_language_id("tf"), Some("terraform"));
        assert_eq!(extension_to_language_id("nix"), Some("nix"));
        assert_eq!(extension_to_language_id("graphql"), Some("graphql"));
        assert_eq!(extension_to_language_id("gql"), Some("graphql"));
        assert_eq!(extension_to_language_id("unknown"), None);
    }

    #[test]
    fn test_find_server_for_language_unknown_returns_none() {
        assert!(find_server_for_language("brainfuck").is_none());
    }

    #[test]
    fn test_find_server_for_language_returns_available_binary() {
        // Only assert on servers we know exist in this dev environment.
        if is_binary_available("rust-analyzer") {
            let rust = find_server_for_language("rust").unwrap();
            assert_eq!(rust.command, "rust-analyzer");
        }
        if is_binary_available("gopls") {
            let go = find_server_for_language("go").unwrap();
            assert_eq!(go.command, "gopls");
        }
    }

    #[test]
    fn test_find_server_skips_unavailable_binary() {
        // "brainfuck" has no config at all → None
        assert!(find_server_for_language("brainfuck").is_none());
        // Languages whose servers are not installed should also return None
        // rather than returning a config that will fail to spawn.
        // (We can't assert a specific language here because it depends on
        // the host, but the logic is: filter by is_binary_available.)
    }

    #[test]
    fn test_language_normalization_in_known_servers() {
        // Verify normalization mappings exist in the known_servers list.
        let servers = known_servers();
        let langs: Vec<&str> = servers.iter().map(|s| s.language_id.as_str()).collect();

        // cpp → "c" (clangd)
        assert!(langs.contains(&"c"), "clangd should be listed under 'c'");
        // javascript → "typescript"
        assert!(
            langs.contains(&"typescript"),
            "ts server should be listed under 'typescript'"
        );
        // scss/less → "css"
        assert!(
            langs.contains(&"css"),
            "css server should be listed under 'css'"
        );
        // hcl → "terraform"
        assert!(
            langs.contains(&"terraform"),
            "terraform-ls should be listed under 'terraform'"
        );
    }

    #[test]
    fn test_is_binary_available() {
        // `which` itself should always be available
        assert!(is_binary_available("ls"));
        assert!(!is_binary_available("nonexistent-binary-xyzzy-12345"));
    }

    #[test]
    fn test_to_repo_symbols_conversion() {
        let range = LspRange {
            start: LspPosition {
                line: 4,
                character: 0,
            },
            end: LspPosition {
                line: 10,
                character: 1,
            },
        };
        let sel = LspRange {
            start: LspPosition {
                line: 4,
                character: 3,
            },
            end: LspPosition {
                line: 4,
                character: 7,
            },
        };

        let lsp_symbols = vec![
            DocumentSymbol {
                name: "MyStruct".into(),
                kind: SymbolKind::Struct,
                range: range.clone(),
                selection_range: sel.clone(),
                children: Some(vec![DocumentSymbol {
                    name: "new".into(),
                    kind: SymbolKind::Method,
                    range: LspRange {
                        start: LspPosition {
                            line: 6,
                            character: 4,
                        },
                        end: LspPosition {
                            line: 8,
                            character: 5,
                        },
                    },
                    selection_range: LspRange {
                        start: LspPosition {
                            line: 6,
                            character: 7,
                        },
                        end: LspPosition {
                            line: 6,
                            character: 10,
                        },
                    },
                    children: None,
                    detail: Some("fn new() -> Self".into()),
                }]),
                detail: None,
            },
            DocumentSymbol {
                name: "main".into(),
                kind: SymbolKind::Function,
                range: range.clone(),
                selection_range: LspRange {
                    start: LspPosition {
                        line: 12,
                        character: 3,
                    },
                    end: LspPosition {
                        line: 12,
                        character: 7,
                    },
                },
                children: None,
                detail: Some("fn main()".into()),
            },
        ];

        let repo_syms = to_repo_symbols(&lsp_symbols, "src/main.rs");

        // 3 total: MyStruct, new (child), main
        assert_eq!(repo_syms.len(), 3);

        assert_eq!(repo_syms[0].name, "MyStruct");
        assert_eq!(repo_syms[0].kind, parser::SymbolKind::Struct);
        assert_eq!(repo_syms[0].line, 5); // 0-indexed line 4 → 1-indexed 5

        assert_eq!(repo_syms[1].name, "new");
        assert_eq!(repo_syms[1].kind, parser::SymbolKind::Function);
        assert_eq!(repo_syms[1].signature.as_deref(), Some("fn new() -> Self"));

        assert_eq!(repo_syms[2].name, "main");
        assert_eq!(repo_syms[2].kind, parser::SymbolKind::Function);
        assert_eq!(repo_syms[2].line, 13);
    }

    #[test]
    fn test_diagnostics_cache_type() {
        let cache: DiagnosticsCache = Arc::new(std::sync::Mutex::new(HashMap::new()));
        {
            let mut c = cache.lock().expect("mutex poisoned");
            c.insert("file:///test.rs".into(), vec![]);
        }
        let c = cache.lock().expect("mutex poisoned");
        assert!(c.contains_key("file:///test.rs"));
        assert!(c.get("file:///test.rs").unwrap().is_empty());
    }
}