use argentor_core::{ArgentorResult, ToolCall, ToolResult};
use argentor_security::{Capability, PermissionSet};
use argentor_skills::skill::{Skill, SkillDescriptor};
use argentor_skills::SkillRegistry;
use async_trait::async_trait;
use std::sync::Arc;
use std::time::Duration;
use tracing::info;
const DEFAULT_BASE_URL: &str = "http://localhost:8000";
fn build_xcapitsff_client() -> reqwest::Client {
#[allow(clippy::expect_used)]
reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.expect("Failed to create HTTP client -- TLS backend unavailable")
}
fn resolve_base_url(base_url: &str) -> String {
if !base_url.is_empty() {
return base_url.trim_end_matches('/').to_string();
}
std::env::var("XCAPITSFF_URL")
.unwrap_or_else(|_| DEFAULT_BASE_URL.to_string())
.trim_end_matches('/')
.to_string()
}
fn host_from_url(base_url: &str) -> String {
reqwest::Url::parse(base_url)
.ok()
.and_then(|u| u.host_str().map(String::from))
.unwrap_or_else(|| "localhost".to_string())
}
pub struct XcapitSearchSkill {
descriptor: SkillDescriptor,
client: reqwest::Client,
base_url: String,
}
impl XcapitSearchSkill {
pub fn new(base_url: &str, client: reqwest::Client) -> Self {
let base = resolve_base_url(base_url);
let host = host_from_url(&base);
Self {
descriptor: SkillDescriptor {
name: "xcapitsff_search".to_string(),
description: "Search across XcapitSFF entities by query string.".to_string(),
parameters_schema: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query string"
}
},
"required": ["query"]
}),
required_capabilities: vec![Capability::NetworkAccess {
allowed_hosts: vec![host],
}],
requires_approval: false,
},
client,
base_url: base,
}
}
}
#[async_trait]
impl Skill for XcapitSearchSkill {
fn descriptor(&self) -> &SkillDescriptor {
&self.descriptor
}
fn validate_arguments(
&self,
call: &ToolCall,
_permissions: &PermissionSet,
) -> ArgentorResult<()> {
let query = call.arguments.get("query").and_then(|v| v.as_str());
if query.is_none() || query.is_some_and(str::is_empty) {
return Err(argentor_core::ArgentorError::Skill(
"Missing or empty 'query' parameter".to_string(),
));
}
Ok(())
}
async fn execute(&self, call: ToolCall) -> ArgentorResult<ToolResult> {
let query = call.arguments["query"]
.as_str()
.unwrap_or_default()
.to_string();
if query.is_empty() {
return Ok(ToolResult::error(
&call.id,
"Missing or empty 'query' parameter",
));
}
let mut parsed = reqwest::Url::parse(&format!("{}/api/v1/search/", self.base_url))
.map_err(|e| argentor_core::ArgentorError::Skill(format!("Invalid base URL: {e}")))?;
parsed.query_pairs_mut().append_pair("q", &query);
let url = parsed.to_string();
info!(skill = "xcapitsff_search", url = %url, "XcapitSFF search");
match self.client.get(&url).send().await {
Ok(resp) => {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
if (200..400).contains(&status) {
Ok(ToolResult::success(&call.id, body))
} else {
Ok(ToolResult::error(
&call.id,
format!("HTTP {status}: {body}"),
))
}
}
Err(e) => Ok(ToolResult::error(&call.id, format!("Request failed: {e}"))),
}
}
}
pub struct XcapitLeadInfoSkill {
descriptor: SkillDescriptor,
client: reqwest::Client,
base_url: String,
}
impl XcapitLeadInfoSkill {
pub fn new(base_url: &str, client: reqwest::Client) -> Self {
let base = resolve_base_url(base_url);
let host = host_from_url(&base);
Self {
descriptor: SkillDescriptor {
name: "xcapitsff_lead_info".to_string(),
description: "Retrieve lead details by ID from XcapitSFF.".to_string(),
parameters_schema: serde_json::json!({
"type": "object",
"properties": {
"lead_id": {
"type": "integer",
"description": "The lead ID to retrieve"
}
},
"required": ["lead_id"]
}),
required_capabilities: vec![Capability::NetworkAccess {
allowed_hosts: vec![host],
}],
requires_approval: false,
},
client,
base_url: base,
}
}
}
#[async_trait]
impl Skill for XcapitLeadInfoSkill {
fn descriptor(&self) -> &SkillDescriptor {
&self.descriptor
}
fn validate_arguments(
&self,
call: &ToolCall,
_permissions: &PermissionSet,
) -> ArgentorResult<()> {
let lead_id = call.arguments.get("lead_id");
if lead_id.is_none() || !lead_id.is_some_and(|v| v.is_i64() || v.is_u64()) {
return Err(argentor_core::ArgentorError::Skill(
"Missing or non-integer 'lead_id' parameter".to_string(),
));
}
Ok(())
}
async fn execute(&self, call: ToolCall) -> ArgentorResult<ToolResult> {
let lead_id = match call.arguments["lead_id"].as_i64() {
Some(id) => id,
None => {
return Ok(ToolResult::error(
&call.id,
"Missing or non-integer 'lead_id' parameter",
));
}
};
let url = format!("{}/api/v1/leads/{}", self.base_url, lead_id);
info!(skill = "xcapitsff_lead_info", url = %url, "XcapitSFF lead info");
match self.client.get(&url).send().await {
Ok(resp) => {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
if (200..400).contains(&status) {
Ok(ToolResult::success(&call.id, body))
} else {
Ok(ToolResult::error(
&call.id,
format!("HTTP {status}: {body}"),
))
}
}
Err(e) => Ok(ToolResult::error(&call.id, format!("Request failed: {e}"))),
}
}
}
pub struct XcapitTicketInfoSkill {
descriptor: SkillDescriptor,
client: reqwest::Client,
base_url: String,
}
impl XcapitTicketInfoSkill {
pub fn new(base_url: &str, client: reqwest::Client) -> Self {
let base = resolve_base_url(base_url);
let host = host_from_url(&base);
Self {
descriptor: SkillDescriptor {
name: "xcapitsff_ticket_info".to_string(),
description: "Retrieve ticket details by ID from XcapitSFF.".to_string(),
parameters_schema: serde_json::json!({
"type": "object",
"properties": {
"ticket_id": {
"type": "integer",
"description": "The ticket ID to retrieve"
}
},
"required": ["ticket_id"]
}),
required_capabilities: vec![Capability::NetworkAccess {
allowed_hosts: vec![host],
}],
requires_approval: false,
},
client,
base_url: base,
}
}
}
#[async_trait]
impl Skill for XcapitTicketInfoSkill {
fn descriptor(&self) -> &SkillDescriptor {
&self.descriptor
}
fn validate_arguments(
&self,
call: &ToolCall,
_permissions: &PermissionSet,
) -> ArgentorResult<()> {
let ticket_id = call.arguments.get("ticket_id");
if ticket_id.is_none() || !ticket_id.is_some_and(|v| v.is_i64() || v.is_u64()) {
return Err(argentor_core::ArgentorError::Skill(
"Missing or non-integer 'ticket_id' parameter".to_string(),
));
}
Ok(())
}
async fn execute(&self, call: ToolCall) -> ArgentorResult<ToolResult> {
let ticket_id = match call.arguments["ticket_id"].as_i64() {
Some(id) => id,
None => {
return Ok(ToolResult::error(
&call.id,
"Missing or non-integer 'ticket_id' parameter",
));
}
};
let url = format!("{}/api/v1/tickets/{}", self.base_url, ticket_id);
info!(skill = "xcapitsff_ticket_info", url = %url, "XcapitSFF ticket info");
match self.client.get(&url).send().await {
Ok(resp) => {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
if (200..400).contains(&status) {
Ok(ToolResult::success(&call.id, body))
} else {
Ok(ToolResult::error(
&call.id,
format!("HTTP {status}: {body}"),
))
}
}
Err(e) => Ok(ToolResult::error(&call.id, format!("Request failed: {e}"))),
}
}
}
pub struct XcapitKbSearchSkill {
descriptor: SkillDescriptor,
client: reqwest::Client,
base_url: String,
}
impl XcapitKbSearchSkill {
pub fn new(base_url: &str, client: reqwest::Client) -> Self {
let base = resolve_base_url(base_url);
let host = host_from_url(&base);
Self {
descriptor: SkillDescriptor {
name: "xcapitsff_kb_search".to_string(),
description: "Search the knowledge base for articles matching a ticket subject and description.".to_string(),
parameters_schema: serde_json::json!({
"type": "object",
"properties": {
"subject": {
"type": "string",
"description": "The ticket subject to search for"
},
"description": {
"type": "string",
"description": "The ticket description to search for"
}
},
"required": ["subject", "description"]
}),
required_capabilities: vec![Capability::NetworkAccess {
allowed_hosts: vec![host],
}],
requires_approval: false,
},
client,
base_url: base,
}
}
}
#[async_trait]
impl Skill for XcapitKbSearchSkill {
fn descriptor(&self) -> &SkillDescriptor {
&self.descriptor
}
fn validate_arguments(
&self,
call: &ToolCall,
_permissions: &PermissionSet,
) -> ArgentorResult<()> {
let subject = call.arguments.get("subject").and_then(|v| v.as_str());
let description = call.arguments.get("description").and_then(|v| v.as_str());
if subject.is_none() || subject.is_some_and(str::is_empty) {
return Err(argentor_core::ArgentorError::Skill(
"Missing or empty 'subject' parameter".to_string(),
));
}
if description.is_none() || description.is_some_and(str::is_empty) {
return Err(argentor_core::ArgentorError::Skill(
"Missing or empty 'description' parameter".to_string(),
));
}
Ok(())
}
async fn execute(&self, call: ToolCall) -> ArgentorResult<ToolResult> {
let subject = call.arguments["subject"]
.as_str()
.unwrap_or_default()
.to_string();
let description = call.arguments["description"]
.as_str()
.unwrap_or_default()
.to_string();
if subject.is_empty() {
return Ok(ToolResult::error(
&call.id,
"Missing or empty 'subject' parameter",
));
}
if description.is_empty() {
return Ok(ToolResult::error(
&call.id,
"Missing or empty 'description' parameter",
));
}
let mut parsed =
reqwest::Url::parse(&format!("{}/api/v1/knowledge/for-ticket", self.base_url))
.map_err(|e| {
argentor_core::ArgentorError::Skill(format!("Invalid base URL: {e}"))
})?;
parsed
.query_pairs_mut()
.append_pair("subject", &subject)
.append_pair("description", &description);
let url = parsed.to_string();
info!(skill = "xcapitsff_kb_search", url = %url, "XcapitSFF KB search");
match self.client.get(&url).send().await {
Ok(resp) => {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
if (200..400).contains(&status) {
Ok(ToolResult::success(&call.id, body))
} else {
Ok(ToolResult::error(
&call.id,
format!("HTTP {status}: {body}"),
))
}
}
Err(e) => Ok(ToolResult::error(&call.id, format!("Request failed: {e}"))),
}
}
}
pub struct XcapitCustomer360Skill {
descriptor: SkillDescriptor,
client: reqwest::Client,
base_url: String,
}
impl XcapitCustomer360Skill {
pub fn new(base_url: &str, client: reqwest::Client) -> Self {
let base = resolve_base_url(base_url);
let host = host_from_url(&base);
Self {
descriptor: SkillDescriptor {
name: "xcapitsff_customer360".to_string(),
description: "Retrieve the 360-degree customer view by customer ID from XcapitSFF."
.to_string(),
parameters_schema: serde_json::json!({
"type": "object",
"properties": {
"customer_id": {
"type": "integer",
"description": "The customer ID to retrieve the 360 view for"
}
},
"required": ["customer_id"]
}),
required_capabilities: vec![Capability::NetworkAccess {
allowed_hosts: vec![host],
}],
requires_approval: false,
},
client,
base_url: base,
}
}
}
#[async_trait]
impl Skill for XcapitCustomer360Skill {
fn descriptor(&self) -> &SkillDescriptor {
&self.descriptor
}
fn validate_arguments(
&self,
call: &ToolCall,
_permissions: &PermissionSet,
) -> ArgentorResult<()> {
let customer_id = call.arguments.get("customer_id");
if customer_id.is_none() || !customer_id.is_some_and(|v| v.is_i64() || v.is_u64()) {
return Err(argentor_core::ArgentorError::Skill(
"Missing or non-integer 'customer_id' parameter".to_string(),
));
}
Ok(())
}
async fn execute(&self, call: ToolCall) -> ArgentorResult<ToolResult> {
let customer_id = match call.arguments["customer_id"].as_i64() {
Some(id) => id,
None => {
return Ok(ToolResult::error(
&call.id,
"Missing or non-integer 'customer_id' parameter",
));
}
};
let url = format!("{}/api/v1/customers/{}/360", self.base_url, customer_id);
info!(skill = "xcapitsff_customer360", url = %url, "XcapitSFF customer 360");
match self.client.get(&url).send().await {
Ok(resp) => {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
if (200..400).contains(&status) {
Ok(ToolResult::success(&call.id, body))
} else {
Ok(ToolResult::error(
&call.id,
format!("HTTP {status}: {body}"),
))
}
}
Err(e) => Ok(ToolResult::error(&call.id, format!("Request failed: {e}"))),
}
}
}
pub fn register_xcapitsff_skills(registry: &SkillRegistry, base_url: &str) {
let client = build_xcapitsff_client();
registry.register(Arc::new(XcapitSearchSkill::new(base_url, client.clone())));
registry.register(Arc::new(XcapitLeadInfoSkill::new(base_url, client.clone())));
registry.register(Arc::new(XcapitTicketInfoSkill::new(
base_url,
client.clone(),
)));
registry.register(Arc::new(XcapitKbSearchSkill::new(base_url, client.clone())));
registry.register(Arc::new(XcapitCustomer360Skill::new(base_url, client)));
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use argentor_security::PermissionSet;
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn make_call(name: &str, args: serde_json::Value) -> ToolCall {
ToolCall {
id: format!("test_{name}"),
name: name.to_string(),
arguments: args,
}
}
#[test]
fn test_search_descriptor_name() {
let client = build_xcapitsff_client();
let skill = XcapitSearchSkill::new("http://example.com", client);
assert_eq!(skill.descriptor().name, "xcapitsff_search");
}
#[test]
fn test_lead_info_descriptor_name() {
let client = build_xcapitsff_client();
let skill = XcapitLeadInfoSkill::new("http://example.com", client);
assert_eq!(skill.descriptor().name, "xcapitsff_lead_info");
}
#[test]
fn test_ticket_info_descriptor_name() {
let client = build_xcapitsff_client();
let skill = XcapitTicketInfoSkill::new("http://example.com", client);
assert_eq!(skill.descriptor().name, "xcapitsff_ticket_info");
}
#[test]
fn test_kb_search_descriptor_name() {
let client = build_xcapitsff_client();
let skill = XcapitKbSearchSkill::new("http://example.com", client);
assert_eq!(skill.descriptor().name, "xcapitsff_kb_search");
}
#[test]
fn test_customer360_descriptor_name() {
let client = build_xcapitsff_client();
let skill = XcapitCustomer360Skill::new("http://example.com", client);
assert_eq!(skill.descriptor().name, "xcapitsff_customer360");
}
#[test]
fn test_descriptor_capabilities_contain_host() {
let client = build_xcapitsff_client();
let skill = XcapitSearchSkill::new("http://my-backend:9000", client);
let caps = &skill.descriptor().required_capabilities;
assert_eq!(caps.len(), 1);
match &caps[0] {
Capability::NetworkAccess { allowed_hosts } => {
assert_eq!(allowed_hosts, &["my-backend".to_string()]);
}
other => panic!("Expected NetworkAccess, got {other:?}"),
}
}
#[test]
fn test_search_validate_missing_query() {
let client = build_xcapitsff_client();
let skill = XcapitSearchSkill::new("http://x", client);
let call = make_call("xcapitsff_search", serde_json::json!({}));
let perms = PermissionSet::new();
assert!(skill.validate_arguments(&call, &perms).is_err());
}
#[test]
fn test_search_validate_empty_query() {
let client = build_xcapitsff_client();
let skill = XcapitSearchSkill::new("http://x", client);
let call = make_call("xcapitsff_search", serde_json::json!({"query": ""}));
let perms = PermissionSet::new();
assert!(skill.validate_arguments(&call, &perms).is_err());
}
#[test]
fn test_lead_validate_missing_id() {
let client = build_xcapitsff_client();
let skill = XcapitLeadInfoSkill::new("http://x", client);
let call = make_call("xcapitsff_lead_info", serde_json::json!({}));
let perms = PermissionSet::new();
assert!(skill.validate_arguments(&call, &perms).is_err());
}
#[test]
fn test_lead_validate_non_integer_id() {
let client = build_xcapitsff_client();
let skill = XcapitLeadInfoSkill::new("http://x", client);
let call = make_call("xcapitsff_lead_info", serde_json::json!({"lead_id": "abc"}));
let perms = PermissionSet::new();
assert!(skill.validate_arguments(&call, &perms).is_err());
}
#[test]
fn test_ticket_validate_missing_id() {
let client = build_xcapitsff_client();
let skill = XcapitTicketInfoSkill::new("http://x", client);
let call = make_call("xcapitsff_ticket_info", serde_json::json!({}));
let perms = PermissionSet::new();
assert!(skill.validate_arguments(&call, &perms).is_err());
}
#[test]
fn test_customer360_validate_missing_id() {
let client = build_xcapitsff_client();
let skill = XcapitCustomer360Skill::new("http://x", client);
let call = make_call("xcapitsff_customer360", serde_json::json!({}));
let perms = PermissionSet::new();
assert!(skill.validate_arguments(&call, &perms).is_err());
}
#[test]
fn test_kb_validate_missing_subject() {
let client = build_xcapitsff_client();
let skill = XcapitKbSearchSkill::new("http://x", client);
let call = make_call(
"xcapitsff_kb_search",
serde_json::json!({"description": "desc"}),
);
let perms = PermissionSet::new();
assert!(skill.validate_arguments(&call, &perms).is_err());
}
#[test]
fn test_kb_validate_missing_description() {
let client = build_xcapitsff_client();
let skill = XcapitKbSearchSkill::new("http://x", client);
let call = make_call(
"xcapitsff_kb_search",
serde_json::json!({"subject": "subj"}),
);
let perms = PermissionSet::new();
assert!(skill.validate_arguments(&call, &perms).is_err());
}
#[tokio::test]
async fn test_search_execute_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/search/"))
.and(query_param("q", "test query"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"results": []})),
)
.mount(&server)
.await;
let client = build_xcapitsff_client();
let skill = XcapitSearchSkill::new(&server.uri(), client);
let call = make_call(
"xcapitsff_search",
serde_json::json!({"query": "test query"}),
);
let result = skill.execute(call).await.unwrap();
assert!(
!result.is_error,
"Expected success, got: {}",
result.content
);
assert!(result.content.contains("results"));
}
#[tokio::test]
async fn test_lead_info_execute_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/leads/42"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"id": 42, "name": "John Doe"})),
)
.mount(&server)
.await;
let client = build_xcapitsff_client();
let skill = XcapitLeadInfoSkill::new(&server.uri(), client);
let call = make_call("xcapitsff_lead_info", serde_json::json!({"lead_id": 42}));
let result = skill.execute(call).await.unwrap();
assert!(
!result.is_error,
"Expected success, got: {}",
result.content
);
assert!(result.content.contains("John Doe"));
}
#[tokio::test]
async fn test_ticket_info_execute_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/tickets/99"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"id": 99, "status": "open"})),
)
.mount(&server)
.await;
let client = build_xcapitsff_client();
let skill = XcapitTicketInfoSkill::new(&server.uri(), client);
let call = make_call(
"xcapitsff_ticket_info",
serde_json::json!({"ticket_id": 99}),
);
let result = skill.execute(call).await.unwrap();
assert!(
!result.is_error,
"Expected success, got: {}",
result.content
);
assert!(result.content.contains("open"));
}
#[tokio::test]
async fn test_kb_search_execute_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/knowledge/for-ticket"))
.and(query_param("subject", "login issue"))
.and(query_param("description", "cannot login"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"articles": [{"title": "Reset password"}]})),
)
.mount(&server)
.await;
let client = build_xcapitsff_client();
let skill = XcapitKbSearchSkill::new(&server.uri(), client);
let call = make_call(
"xcapitsff_kb_search",
serde_json::json!({"subject": "login issue", "description": "cannot login"}),
);
let result = skill.execute(call).await.unwrap();
assert!(
!result.is_error,
"Expected success, got: {}",
result.content
);
assert!(result.content.contains("Reset password"));
}
#[tokio::test]
async fn test_customer360_execute_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/customers/7/360"))
.respond_with(ResponseTemplate::new(200).set_body_json(
serde_json::json!({"customer_id": 7, "name": "Acme Corp", "tickets": 3}),
))
.mount(&server)
.await;
let client = build_xcapitsff_client();
let skill = XcapitCustomer360Skill::new(&server.uri(), client);
let call = make_call(
"xcapitsff_customer360",
serde_json::json!({"customer_id": 7}),
);
let result = skill.execute(call).await.unwrap();
assert!(
!result.is_error,
"Expected success, got: {}",
result.content
);
assert!(result.content.contains("Acme Corp"));
}
#[tokio::test]
async fn test_execute_returns_error_on_404() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/leads/999"))
.respond_with(ResponseTemplate::new(404).set_body_string("not found"))
.mount(&server)
.await;
let client = build_xcapitsff_client();
let skill = XcapitLeadInfoSkill::new(&server.uri(), client);
let call = make_call("xcapitsff_lead_info", serde_json::json!({"lead_id": 999}));
let result = skill.execute(call).await.unwrap();
assert!(result.is_error, "Expected error on 404");
assert!(result.content.contains("404"));
}
#[tokio::test]
async fn test_execute_returns_error_on_500() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/tickets/1"))
.respond_with(ResponseTemplate::new(500).set_body_string("internal server error"))
.mount(&server)
.await;
let client = build_xcapitsff_client();
let skill = XcapitTicketInfoSkill::new(&server.uri(), client);
let call = make_call("xcapitsff_ticket_info", serde_json::json!({"ticket_id": 1}));
let result = skill.execute(call).await.unwrap();
assert!(result.is_error, "Expected error on 500");
assert!(result.content.contains("500"));
}
#[tokio::test]
async fn test_search_execute_empty_query_returns_error() {
let client = build_xcapitsff_client();
let skill = XcapitSearchSkill::new("http://unused", client);
let call = make_call("xcapitsff_search", serde_json::json!({"query": ""}));
let result = skill.execute(call).await.unwrap();
assert!(result.is_error);
assert!(result.content.contains("query"));
}
#[test]
fn test_register_xcapitsff_skills_registers_all_five() {
let registry = SkillRegistry::new();
register_xcapitsff_skills(®istry, "http://test-backend:8000");
assert!(registry.get("xcapitsff_search").is_some());
assert!(registry.get("xcapitsff_lead_info").is_some());
assert!(registry.get("xcapitsff_ticket_info").is_some());
assert!(registry.get("xcapitsff_kb_search").is_some());
assert!(registry.get("xcapitsff_customer360").is_some());
}
#[test]
fn test_resolve_base_url_uses_provided() {
let url = resolve_base_url("http://custom:9000/");
assert_eq!(url, "http://custom:9000");
}
#[test]
fn test_resolve_base_url_strips_trailing_slash() {
let url = resolve_base_url("http://example.com/");
assert_eq!(url, "http://example.com");
}
#[test]
fn test_host_from_url_extracts_host() {
assert_eq!(host_from_url("http://my-host:8000"), "my-host");
assert_eq!(
host_from_url("https://api.example.com/path"),
"api.example.com"
);
}
}