use pretty_assertions::assert_eq;
use serde_json::json;
use rho_tools::tool_card::{ToolFact, ToolHeader};
use super::{argument_summary, mcp_header_and_facts, primary_argument};
use crate::tools::mcp::exported_name::{parse_exported_name, ExportedNameDialect, McpToolIdentity};
#[test]
fn parse_exported_name_splits_server_and_tool() {
let cases: &[(&str, Option<(&str, &str)>)] = &[
("mcp__filesystem__read", Some(("filesystem", "read"))),
("mcp__srv__a__b", Some(("srv", "a__b"))),
("bash", None),
("mcp__only", None),
("mcp____tool", None),
("mcp__srv__", None),
];
for (name, expected) in cases {
let expected = expected.map(|(server, tool)| McpToolIdentity {
server: server.into(),
tool: tool.into(),
});
assert_eq!(
parse_exported_name(name, ExportedNameDialect::Rho),
expected,
"name: {name}"
);
}
}
#[test]
fn parse_exported_name_decodes_rho_escapes() {
let parsed = parse_exported_name(
"mcp___rho_6769742d687562___rho_6973737565732f6c697374",
ExportedNameDialect::Rho,
)
.unwrap();
assert_eq!(
parsed,
McpToolIdentity {
server: "git-hub".into(),
tool: "issues/list".into(),
}
);
let parsed = parse_exported_name("mcp___rho_zz__tool", ExportedNameDialect::Rho).unwrap();
assert_eq!(parsed.server, "_rho_zz");
assert_eq!(parsed.tool, "tool");
}
#[test]
fn primary_argument_prefers_known_keys_then_single_string() {
let args = json!({"note": "misc", "path": "crates/rho", "count": 3});
assert_eq!(
primary_argument(&args),
Some(("path".into(), "crates/rho".into()))
);
let args = json!({"target": "src/main.rs", "count": 3});
assert_eq!(
primary_argument(&args),
Some(("target".into(), "src/main.rs".into()))
);
let args = json!({"a": "x", "b": "y"});
assert_eq!(primary_argument(&args), None);
let args = json!({"prompt": "first line\nsecond line"});
assert_eq!(
primary_argument(&args),
Some(("prompt".into(), "first line…".into()))
);
assert_eq!(primary_argument(&json!({})), None);
assert_eq!(primary_argument(&json!("not an object")), None);
}
#[test]
fn argument_summary_joins_scalars_and_skips_primary() {
let args = json!({
"path": "crates",
"output_mode": "files_with_matches",
"max_results": 50,
"literal": true,
"body": "line one\nline two",
"extra": {"nested": 1},
"items": [1, 2],
});
assert_eq!(
argument_summary(&args, Some("path")).unwrap(),
"output_mode files_with_matches · max_results 50 · literal true · extra {…} · items […]"
);
assert_eq!(argument_summary(&json!({}), None), None);
assert_eq!(
argument_summary(&json!({"body": "a\nb"}), None),
None,
"only a multiline string leaves nothing to summarize"
);
}
#[test]
fn mcp_header_and_facts_assembles_shared_grammar() {
let args = json!({"path": "crates", "max_results": 50});
let (header, facts) =
mcp_header_and_facts("mcp__olive__grep", Some(&args), ExportedNameDialect::Rho).unwrap();
assert_eq!(header, ToolHeader::call("grep", Some("crates".into())));
assert_eq!(
facts,
vec![
ToolFact::Meta {
text: "mcp · olive".into()
},
ToolFact::Text {
text: "max_results 50".into()
},
]
);
let (header, facts) =
mcp_header_and_facts("mcp__olive__grep", None, ExportedNameDialect::Rho).unwrap();
assert_eq!(header, ToolHeader::call("grep", None));
assert_eq!(
facts,
vec![ToolFact::Meta {
text: "mcp · olive".into()
}]
);
assert_eq!(
mcp_header_and_facts("bash", None, ExportedNameDialect::Rho),
None
);
}
#[test]
fn primary_argument_multiline_stays_within_budget() {
for len in [79, 80, 81] {
let first_line = "x".repeat(len);
let args = json!({ "prompt": format!("{first_line}\nmore") });
let (_, display) = primary_argument(&args).unwrap();
assert!(
display.chars().count() <= 80,
"len {len}: {} chars",
display.chars().count()
);
assert!(display.ends_with('…'), "len {len}");
}
}
#[test]
fn display_text_flattens_control_characters() {
let args = json!({
"x\ninjected": "ok",
"note": "a\rb\tc",
"path": "crates/rho\rpayload",
});
assert_eq!(
primary_argument(&args),
Some(("path".into(), "crates/rho payload".into()))
);
assert_eq!(
argument_summary(&args, Some("path")).as_deref(),
Some("x injected ok · note a b c")
);
let (header, facts) = mcp_header_and_facts(
"mcp__olive\rsrv__grep\ttool",
Some(&args),
ExportedNameDialect::Rho,
)
.unwrap();
assert_eq!(
header,
ToolHeader::call("grep tool", Some("crates/rho payload".into()))
);
assert_eq!(
facts,
vec![
ToolFact::Meta {
text: "mcp · olive srv".into()
},
ToolFact::Text {
text: "x injected ok · note a b c".into()
},
]
);
}
#[test]
fn display_text_truncates_after_control_normalization() {
let first = format!("{}x", "\t".repeat(80));
let args = json!({ "prompt": format!("{first}\nmore") });
let (_, display) = primary_argument(&args).unwrap();
assert!(display.chars().count() <= 80);
assert!(!display.contains(char::is_control));
assert!(display.ends_with('…'));
let args = json!({ "note": format!("{}y", "\r".repeat(200)) });
let summary = argument_summary(&args, None).unwrap();
assert!(summary.chars().count() <= 160);
assert!(!summary.contains(char::is_control));
assert!(summary.starts_with("note "));
}