Skip to main content
Glama
fulltext_search_test.exs12.4 kB
defmodule HexdocsMcp.CLI.FulltextSearchTest do use ExUnit.Case, async: true import ExUnit.CaptureIO import Mox alias HexdocsMcp.CLI.FulltextSearch setup :verify_on_exit! setup do system_command = HexdocsMcp.Config.system_command() [system_command: system_command] end describe "fulltext_search command" do test "searching across all packages" do expect(HexdocsMcp.MockFulltextSearch, :search, fn "GenServer", opts -> assert Keyword.get(opts, :limit) == 3 assert Keyword.get(opts, :package) == nil assert Keyword.get(opts, :version) == nil search_info = %{ total_found: 150, page: 1, per_page: 3, search_time_ms: 42 } results = [ %{ package: "elixir", ref: "GenServer.html", title: "GenServer", type: "module", url: "https://hexdocs.pm/elixir/GenServer.html", snippet: "A <mark>GenServer</mark> is a process like any other Elixir process...", matched_tokens: ["GenServer"], score: 0.95, proglang: "elixir", highlights: [%{field: "doc", snippet: "A <mark>GenServer</mark> is a process"}] }, %{ package: "elixir", ref: "GenServer.html#call/3", title: "GenServer.call/3", type: "function", url: "https://hexdocs.pm/elixir/GenServer.html#call/3", snippet: "Makes a synchronous call to the <mark>GenServer</mark>...", matched_tokens: ["GenServer"], score: 0.90, proglang: "elixir", highlights: [] } ] {:ok, results, search_info} end) output = capture_io(fn -> FulltextSearch.main(["--query", "GenServer", "--limit", "3"]) end) assert output =~ "Searching HexDocs in all packages for \"GenServer\"" assert output =~ "Found 150 results (showing 2)" assert output =~ "GenServer" assert output =~ "Package: elixir" assert output =~ "Type: module" assert output =~ "Type: function" assert output =~ "Match:" assert output =~ "URL: https://hexdocs.pm/" end test "searching within a specific package" do expect(HexdocsMcp.MockFulltextSearch, :search, fn "LiveView", opts -> assert Keyword.get(opts, :package) == "phoenix" assert Keyword.get(opts, :limit) == 3 search_info = %{ total_found: 25, page: 1, per_page: 3, search_time_ms: 15 } results = [ %{ package: "phoenix", ref: "Phoenix.LiveView.html", title: "Phoenix.LiveView", type: "module", url: "https://hexdocs.pm/phoenix/Phoenix.LiveView.html", snippet: "The <mark>LiveView</mark> behaviour defines...", matched_tokens: ["LiveView"], score: 0.98, proglang: "elixir", highlights: [] } ] {:ok, results, search_info} end) output = capture_io(fn -> FulltextSearch.main(["phoenix", "--query", "LiveView", "--limit", "3"]) end) assert output =~ "Searching HexDocs in phoenix for \"LiveView\"" assert output =~ "Found 25 results (showing 1)" assert output =~ "Phoenix.LiveView" assert output =~ "Package: phoenix" assert output =~ "Type: module" end test "searching within a specific package version" do expect(HexdocsMcp.MockFulltextSearch, :search, fn "router", opts -> assert Keyword.get(opts, :package) == "phoenix" assert Keyword.get(opts, :version) == "1.7.14" assert Keyword.get(opts, :limit) == 3 search_info = %{ total_found: 10, page: 1, per_page: 3, search_time_ms: 8 } results = [ %{ package: "phoenix-1.7.14", ref: "Phoenix.Router.html", title: "Phoenix.Router", type: "module", url: "https://hexdocs.pm/phoenix/1.7.14/Phoenix.Router.html", snippet: "Defines a Phoenix <mark>router</mark>...", matched_tokens: ["router"], score: 0.92, proglang: "elixir", highlights: [] } ] {:ok, results, search_info} end) output = capture_io(fn -> FulltextSearch.main(["phoenix", "1.7.14", "--query", "router", "--limit", "3"]) end) assert output =~ "Searching HexDocs in phoenix v1.7.14 for \"router\"" assert output =~ "Found 10 results (showing 1)" assert output =~ "Phoenix.Router" assert output =~ "Package: phoenix-1.7.14" end test "handles unusual query" do expect(HexdocsMcp.MockFulltextSearch, :search, fn "xyz_nonexistent_query_abc_123_456_789", _opts -> search_info = %{ total_found: 0, page: 1, per_page: 10, search_time_ms: 5 } {:ok, [], search_info} end) output = capture_io(fn -> FulltextSearch.main(["--query", "xyz_nonexistent_query_abc_123_456_789"]) end) assert output =~ "Searching HexDocs" assert output =~ "No results found for \"xyz_nonexistent_query_abc_123_456_789\"" end test "displays query syntax in help", %{system_command: system_command} do output = capture_io(fn -> FulltextSearch.main(["--help"]) end) assert output =~ "Usage: #{system_command} fulltext_search" assert output =~ "Performs full-text search on HexDocs documentation using Typesense" assert output =~ "Query Syntax:" assert output =~ "Basic search:" assert output =~ "Exact phrase:" assert output =~ "AND operator:" assert output =~ "OR operator:" assert output =~ "Exclude terms:" assert output =~ "Examples:" end test "requires query parameter" do output = capture_io(fn -> FulltextSearch.main([]) end) assert output =~ "Usage:" assert output =~ "--query" end test "supports AND operator in query" do expect(HexdocsMcp.MockFulltextSearch, :search, fn "Phoenix AND LiveView", opts -> assert Keyword.get(opts, :limit) == 2 search_info = %{ total_found: 5, page: 1, per_page: 2, search_time_ms: 10 } results = [ %{ package: "phoenix_live_view", ref: "Phoenix.LiveView.html", title: "Phoenix.LiveView", type: "module", url: "https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html", snippet: "<mark>Phoenix</mark> <mark>LiveView</mark> enables rich, real-time user experiences...", matched_tokens: ["Phoenix", "LiveView"], score: 0.99, proglang: "elixir", highlights: [] } ] {:ok, results, search_info} end) output = capture_io(fn -> FulltextSearch.main(["--query", "Phoenix AND LiveView", "--limit", "2"]) end) assert output =~ "Searching HexDocs in all packages for \"Phoenix AND LiveView\"" assert output =~ "Phoenix.LiveView" end test "supports exact phrase search" do expect(HexdocsMcp.MockFulltextSearch, :search, fn "\"handle_event\"", opts -> assert Keyword.get(opts, :limit) == 2 search_info = %{ total_found: 10, page: 1, per_page: 2, search_time_ms: 8 } results = [ %{ package: "phoenix_live_view", ref: "Phoenix.LiveView.html#handle_event/3", title: "handle_event/3", type: "callback", url: "https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#handle_event/3", snippet: "The <mark>handle_event</mark> callback is invoked when...", matched_tokens: ["handle_event"], score: 0.95, proglang: "elixir", highlights: [] } ] {:ok, results, search_info} end) output = capture_io(fn -> FulltextSearch.main(["--query", "\"handle_event\"", "--limit", "2"]) end) assert output =~ "Searching HexDocs in all packages for" assert output =~ "handle_event" assert output =~ "handle_event/3" end test "supports exclude operator" do expect(HexdocsMcp.MockFulltextSearch, :search, fn "Phoenix -test", opts -> assert Keyword.get(opts, :limit) == 2 search_info = %{ total_found: 50, page: 1, per_page: 2, search_time_ms: 15 } results = [ %{ package: "phoenix", ref: "Phoenix.html", title: "Phoenix", type: "module", url: "https://hexdocs.pm/phoenix/Phoenix.html", snippet: "<mark>Phoenix</mark> is a web framework for Elixir...", matched_tokens: ["Phoenix"], score: 0.90, proglang: "elixir", highlights: [] } ] {:ok, results, search_info} end) output = capture_io(fn -> FulltextSearch.main(["--query", "Phoenix -test", "--limit", "2"]) end) assert output =~ "Searching HexDocs in all packages for \"Phoenix -test\"" assert output =~ "Phoenix" # The search query itself contains "test", so we can't refute it entirely end test "respects limit parameter" do expect(HexdocsMcp.MockFulltextSearch, :search, fn "function", opts -> assert Keyword.get(opts, :limit) == 1 search_info = %{ total_found: 1000, page: 1, per_page: 1, search_time_ms: 20 } results = [ %{ package: "elixir", ref: "Function.html", title: "Function", type: "module", url: "https://hexdocs.pm/elixir/Function.html", snippet: "Functions in Elixir...", matched_tokens: ["function"], score: 0.88, proglang: "elixir", highlights: [] } ] {:ok, results, search_info} end) output = capture_io(fn -> FulltextSearch.main(["--query", "function", "--limit", "1"]) end) assert output =~ "Found 1000 results (showing 1)" # Count the number of URLs in output - should be exactly 1 url_count = output |> String.split("\n") |> Enum.count(&String.contains?(&1, "URL:")) assert url_count == 1 end test "shows snippet highlighting" do expect(HexdocsMcp.MockFulltextSearch, :search, fn "GenServer", opts -> assert Keyword.get(opts, :limit) == 1 search_info = %{ total_found: 50, page: 1, per_page: 1, search_time_ms: 10 } results = [ %{ package: "elixir", ref: "GenServer.html", title: "GenServer", type: "module", url: "https://hexdocs.pm/elixir/GenServer.html", snippet: "A <mark>GenServer</mark> is a process like any other Elixir process...", matched_tokens: ["GenServer"], score: 0.95, proglang: "elixir", highlights: [] } ] {:ok, results, search_info} end) output = capture_io(fn -> FulltextSearch.main(["--query", "GenServer", "--limit", "1"]) end) assert output =~ "Match:" # The CLI should replace <mark> tags with ANSI formatting refute output =~ "<mark>" refute output =~ "</mark>" end test "handles API errors gracefully" do expect(HexdocsMcp.MockFulltextSearch, :search, fn "test", _opts -> {:error, "Search API is temporarily unavailable"} end) output = capture_io(:stderr, fn -> capture_io(fn -> FulltextSearch.main(["--query", "test"]) end) end) assert output =~ "Search failed: Search API is temporarily unavailable" end end end

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bradleygolden/hexdocs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server