use pretty_assertions::assert_eq;
use serde_json::json;
use super::{search_entries, ToolCatalogEntry};
fn entry(name: &str, description: &str) -> ToolCatalogEntry {
ToolCatalogEntry {
name: name.into(),
description: description.into(),
parameters: json!({}),
returns: None,
}
}
fn with_parameters(mut entry: ToolCatalogEntry, names: &[&str]) -> ToolCatalogEntry {
let properties: serde_json::Map<_, _> = names
.iter()
.map(|name| ((*name).to_owned(), json!({"type": "string"})))
.collect();
entry.parameters = json!({"type": "object", "properties": properties});
entry
}
fn assert_rankings(catalog: &[ToolCatalogEntry], cases: &[(&str, Vec<&str>)]) {
for (query, expected) in cases {
for limit in [0, 1, catalog.len(), usize::MAX] {
let names: Vec<&str> = search_entries(catalog, query, limit)
.into_iter()
.map(|hit| hit.name.as_str())
.collect();
assert_eq!(
(*query, limit, names.as_slice()),
(*query, limit, &expected[..expected.len().min(limit)])
);
}
}
}
#[test]
fn search_matches_any_term_and_ranks_name_hits_first() {
let catalog = [
entry("bash", "Run shell commands."),
entry("mcp__memorywhale__remember", "Save a lesson to memory."),
entry(
"mcp__memorywhale__search_memory",
"Full-text search over memory.",
),
];
assert_rankings(
&catalog,
&[
(
"+memorywhale remember",
vec![
"mcp__memorywhale__remember",
"mcp__memorywhale__search_memory",
],
),
(
"search MEMORY",
vec![
"mcp__memorywhale__search_memory",
"mcp__memorywhale__remember",
],
),
(
"mcp__memorywhale__remember",
vec![
"mcp__memorywhale__remember",
"mcp__memorywhale__search_memory",
],
),
(
" ",
vec![
"bash",
"mcp__memorywhale__remember",
"mcp__memorywhale__search_memory",
],
),
("shell remember", vec!["mcp__memorywhale__remember", "bash"]),
("post a message to slack", vec![]),
("github", vec![]),
],
);
}
#[test]
fn search_normalizes_case_styles_separators_and_inflections() {
let catalog = [
entry("mcp__github__create_issue", "Open a new issue."),
entry("mcp__github__list_issues", "List issues in a repository."),
entry("mcp__playwright__browser_navigate", "Navigate to a URL."),
entry("str_replace", "Replace exact text in a file."),
entry(
"openapi__github__repos_getContent",
"Get repository content\n\nGET /repos/{owner}/{repo}/contents/{path} on OpenAPI source `github` (tags: repos).",
),
];
assert_rankings(
&catalog,
&[
(
"createIssue",
vec!["mcp__github__create_issue", "mcp__github__list_issues"],
),
(
"listing issues",
vec!["mcp__github__list_issues", "mcp__github__create_issue"],
),
(
"browser-navigate",
vec!["mcp__playwright__browser_navigate"],
),
(
"repos.get_content",
vec![
"openapi__github__repos_getContent",
"mcp__github__list_issues",
],
),
(
"mcp",
vec![
"mcp__github__create_issue",
"mcp__github__list_issues",
"mcp__playwright__browser_navigate",
],
),
("getting content", vec!["openapi__github__repos_getContent"]),
("string", vec![]),
("a to", vec![]),
("URLs", vec!["mcp__playwright__browser_navigate"]),
(
"GitHub",
vec![
"mcp__github__create_issue",
"mcp__github__list_issues",
"openapi__github__repos_getContent",
],
),
(
"github",
vec![
"mcp__github__create_issue",
"mcp__github__list_issues",
"openapi__github__repos_getContent",
],
),
(
"GITHUB",
vec![
"mcp__github__create_issue",
"mcp__github__list_issues",
"openapi__github__repos_getContent",
],
),
],
);
}
#[test]
fn search_ranks_an_exact_name_first() {
let catalog = [
entry("mcp__github__get-issue", "Get a GitHub issue."),
entry("mcp__github__get_issue", "Get a GitHub issue."),
];
for name in ["mcp__github__get-issue", "mcp__github__get_issue"] {
let first = search_entries(&catalog, name, 1)
.first()
.map(|hit| hit.name.as_str());
assert_eq!((name, first), (name, Some(name)));
}
}
#[test]
fn search_prefers_specific_matches_and_rare_terms() {
let mut catalog = vec![
entry(
"openapi__github__actions_get_permissions_repository",
"Get GitHub Actions permissions for a repository\n\nGET /repos/{owner}/{repo}/actions/permissions on OpenAPI source `github` (tags: actions).",
),
entry(
"openapi__github__repos_get",
"Get a repository\n\nGET /repos/{owner}/{repo} on OpenAPI source `github` (tags: repos).",
),
entry(
"openapi__github__repos_get_webhook",
"Get a repository webhook\n\nGET /repos/{owner}/{repo}/hooks/{hook_id} on OpenAPI source `github` (tags: repos).",
),
with_parameters(
entry(
"openapi__github__repos_list_for_org",
"List organization repositories\n\nGET /orgs/{org}/repos on OpenAPI source `github` (tags: repos).",
),
&["org", "type", "sort"],
),
];
let expected_first = [
("get repository", "openapi__github__repos_get"),
("repository webhook", "openapi__github__repos_get_webhook"),
("sort repositories", "openapi__github__repos_list_for_org"),
];
catalog.extend([
entry(
"mcp__sqlite__query_database",
"Run a SQL query on the database.",
),
entry("mcp__sqlite__create_table", "Create a table for data."),
entry("mcp__sqlite__create_record", "Create a data record."),
entry("mcp__sqlite__create_view", "Create a view over data."),
]);
catalog.sort_by(|left, right| left.name.cmp(&right.name));
let expected_first = expected_first
.into_iter()
.chain([("create database", "mcp__sqlite__query_database")]);
for (query, expected) in expected_first {
let first = search_entries(&catalog, query, 1)
.first()
.map(|hit| hit.name.as_str());
assert_eq!((query, first), (query, Some(expected)));
}
}