#
tokens: 32736/50000 1/231 files (page 9/11)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 9 of 11. Use http://codebase.md/tuananh/hyper-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       └── print-ctx-size.mdc
├── .dockerignore
├── .github
│   ├── renovate.json5
│   └── workflows
│       ├── ci.yml
│       ├── nightly.yml
│       └── release.yml
├── .gitignore
├── .gitmodules
├── .hadolint.yaml
├── .pre-commit-config.yaml
├── .windsurf
│   └── rules
│       ├── print-ctx-size.md
│       └── think.md
├── assets
│   ├── cursor-mcp-1.png
│   ├── cursor-mcp.png
│   ├── eval-py.jpg
│   └── logo.png
├── Cargo.lock
├── Cargo.toml
├── config.example.json
├── config.example.yaml
├── CREATING_PLUGINS.md
├── DEPLOYMENT.md
├── Dockerfile
├── examples
│   └── plugins
│       ├── v1
│       │   ├── arxiv
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── context7
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── crates-io
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── crypto-price
│       │   │   ├── Dockerfile
│       │   │   ├── go.mod
│       │   │   ├── go.sum
│       │   │   ├── main.go
│       │   │   ├── pdk.gen.go
│       │   │   └── README.md
│       │   ├── eval-py
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── fetch
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── fs
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── github
│       │   │   ├── .gitignore
│       │   │   ├── branches.go
│       │   │   ├── Dockerfile
│       │   │   ├── files.go
│       │   │   ├── gists.go
│       │   │   ├── go.mod
│       │   │   ├── go.sum
│       │   │   ├── issues.go
│       │   │   ├── main.go
│       │   │   ├── pdk.gen.go
│       │   │   ├── README.md
│       │   │   └── repo.go
│       │   ├── gitlab
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── gomodule
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── hash
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── maven
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── meme-generator
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── generate_embedded.py
│       │   │   ├── README.md
│       │   │   ├── src
│       │   │   │   ├── embedded.rs
│       │   │   │   ├── lib.rs
│       │   │   │   └── pdk.rs
│       │   │   └── templates.json
│       │   ├── memory
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── myip
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── qdrant
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       ├── pdk.rs
│       │   │       └── qdrant_client.rs
│       │   ├── qr-code
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── serper
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── sqlite
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── think
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── time
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   ├── src
│       │   │   │   ├── lib.rs
│       │   │   │   └── pdk.rs
│       │   │   └── time.wasm
│       │   └── tool-list-changed
│       │       ├── .gitignore
│       │       ├── Cargo.toml
│       │       ├── Dockerfile
│       │       ├── README.md
│       │       ├── src
│       │       │   ├── lib.rs
│       │       │   └── pdk.rs
│       │       └── tool_list_changed.wasm
│       └── v2
│           └── rstime
│               ├── .cargo
│               │   └── config.toml
│               ├── .gitignore
│               ├── Cargo.toml
│               ├── Dockerfile
│               ├── README.md
│               ├── rstime.wasm
│               └── src
│                   ├── lib.rs
│                   └── pdk
│                       ├── exports.rs
│                       ├── imports.rs
│                       ├── mod.rs
│                       └── types.rs
├── iac
│   ├── .terraform.lock.hcl
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── justfile
├── LICENSE
├── README.md
├── RUNTIME_CONFIG.md
├── rust-toolchain.toml
├── server.json
├── SKIP_TOOLS_GUIDE.md
├── src
│   ├── cli.rs
│   ├── config.rs
│   ├── https_auth.rs
│   ├── logging.rs
│   ├── main.rs
│   ├── naming.rs
│   ├── plugin.rs
│   ├── service.rs
│   └── wasm
│       ├── http.rs
│       ├── mod.rs
│       ├── oci.rs
│       └── s3.rs
├── templates
│   └── plugins
│       ├── go
│       │   ├── .gitignore
│       │   ├── Dockerfile
│       │   ├── exports.go
│       │   ├── go.mod
│       │   ├── go.sum
│       │   ├── imports.go
│       │   ├── main.go
│       │   ├── README.md
│       │   └── types.go
│       ├── README.md
│       └── rust
│           ├── .cargo
│           │   └── config.toml
│           ├── .gitignore
│           ├── Cargo.toml
│           ├── Dockerfile
│           ├── README.md
│           └── src
│               ├── lib.rs
│               └── pdk
│                   ├── exports.rs
│                   ├── imports.rs
│                   ├── mod.rs
│                   └── types.rs
├── tests
│   └── fixtures
│       ├── config_with_auths.json
│       ├── config_with_auths.yaml
│       ├── documentation_example.json
│       ├── documentation_example.yaml
│       ├── invalid_auth_config.yaml
│       ├── invalid_plugin_name.yaml
│       ├── invalid_structure.yaml
│       ├── invalid_url.yaml
│       ├── keyring_auth_config.yaml
│       ├── skip_tools_examples.yaml
│       ├── unsupported_config.txt
│       ├── valid_config.json
│       └── valid_config.yaml
└── xtp-plugin-schema.json
```

# Files

--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------

```rust
   1 | use crate::cli::Cli;
   2 | use anyhow::{Context, Result};
   3 | use once_cell::sync::Lazy;
   4 | use regex::{Regex, RegexSet};
   5 | use serde::{Deserialize, Serialize};
   6 | use std::{collections::HashMap, convert::TryFrom, fmt, path::PathBuf, str::FromStr};
   7 | use url::Url;
   8 | 
   9 | #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
  10 | pub struct PluginName(String);
  11 | 
  12 | #[derive(Clone, Debug)]
  13 | pub struct PluginNameParseError;
  14 | 
  15 | impl fmt::Display for PluginNameParseError {
  16 |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  17 |         write!(f, "Failed to parse plugin name")
  18 |     }
  19 | }
  20 | 
  21 | impl std::error::Error for PluginNameParseError {}
  22 | 
  23 | static PLUGIN_NAME_REGEX: Lazy<Regex> = Lazy::new(|| {
  24 |     Regex::new(r"^[A-Za-z0-9]+(?:[_][A-Za-z0-9]+)*$").expect("Failed to compile plugin name regex")
  25 | });
  26 | 
  27 | impl PluginName {
  28 |     #[allow(dead_code)]
  29 |     pub fn as_str(&self) -> &str {
  30 |         &self.0
  31 |     }
  32 | }
  33 | 
  34 | impl<'de> Deserialize<'de> for PluginName {
  35 |     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  36 |     where
  37 |         D: serde::Deserializer<'de>,
  38 |     {
  39 |         let s = String::deserialize(deserializer)?;
  40 |         PluginName::try_from(s.as_str()).map_err(serde::de::Error::custom)
  41 |     }
  42 | }
  43 | 
  44 | impl TryFrom<&str> for PluginName {
  45 |     type Error = PluginNameParseError;
  46 | 
  47 |     fn try_from(value: &str) -> Result<Self, Self::Error> {
  48 |         if PLUGIN_NAME_REGEX.is_match(value) {
  49 |             Ok(PluginName(value.to_owned()))
  50 |         } else {
  51 |             Err(PluginNameParseError)
  52 |         }
  53 |     }
  54 | }
  55 | 
  56 | impl TryFrom<String> for PluginName {
  57 |     type Error = PluginNameParseError;
  58 | 
  59 |     fn try_from(value: String) -> Result<Self, Self::Error> {
  60 |         PluginName::try_from(value.as_str())
  61 |     }
  62 | }
  63 | 
  64 | impl TryFrom<&String> for PluginName {
  65 |     type Error = PluginNameParseError;
  66 | 
  67 |     fn try_from(value: &String) -> Result<Self, Self::Error> {
  68 |         PluginName::try_from(value.as_str())
  69 |     }
  70 | }
  71 | 
  72 | impl FromStr for PluginName {
  73 |     type Err = PluginNameParseError;
  74 | 
  75 |     fn from_str(s: &str) -> Result<Self, Self::Err> {
  76 |         PluginName::try_from(s)
  77 |     }
  78 | }
  79 | 
  80 | impl fmt::Display for PluginName {
  81 |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  82 |         write!(f, "{}", self.0)
  83 |     }
  84 | }
  85 | 
  86 | #[derive(Clone, Debug, Serialize)]
  87 | #[serde(tag = "type", rename_all = "lowercase")]
  88 | pub enum AuthConfig {
  89 |     Basic { username: String, password: String },
  90 |     Token { token: String },
  91 | }
  92 | 
  93 | #[derive(Debug, Deserialize, Serialize)]
  94 | #[serde(tag = "type", rename_all = "lowercase")]
  95 | enum InternalAuthConfig {
  96 |     Basic { username: String, password: String },
  97 |     Keyring { service: String, user: String },
  98 |     Token { token: String },
  99 | }
 100 | 
 101 | impl<'de> Deserialize<'de> for AuthConfig {
 102 |     fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
 103 |     where
 104 |         D: serde::Deserializer<'de>,
 105 |     {
 106 |         let internal = InternalAuthConfig::deserialize(deserializer)?;
 107 |         match internal {
 108 |             InternalAuthConfig::Basic { username, password } => {
 109 |                 Ok(AuthConfig::Basic { username, password })
 110 |             }
 111 |             InternalAuthConfig::Token { token } => Ok(AuthConfig::Token { token }),
 112 |             InternalAuthConfig::Keyring { service, user } => {
 113 |                 use keyring::Entry;
 114 |                 use serde::de;
 115 | 
 116 |                 let entry =
 117 |                     Entry::new(service.as_str(), user.as_str()).map_err(de::Error::custom)?;
 118 |                 let secret = entry.get_secret().map_err(de::Error::custom)?;
 119 |                 Ok(serde_json::from_slice::<AuthConfig>(secret.as_slice())
 120 |                     .map_err(de::Error::custom)?)
 121 |             }
 122 |         }
 123 |     }
 124 | }
 125 | 
 126 | #[derive(Clone, Debug, Default, Deserialize, Serialize)]
 127 | pub struct Config {
 128 |     #[serde(skip_serializing_if = "Option::is_none")]
 129 |     pub auths: Option<HashMap<Url, AuthConfig>>,
 130 | 
 131 |     #[serde(default)]
 132 |     pub oci: OciConfig,
 133 | 
 134 |     pub plugins: HashMap<PluginName, PluginConfig>,
 135 | }
 136 | 
 137 | #[derive(Clone, Debug, Deserialize, Serialize)]
 138 | pub struct OciConfig {
 139 |     #[serde(skip_serializing_if = "Option::is_none")]
 140 |     pub cert_email: Option<String>,
 141 | 
 142 |     #[serde(skip_serializing_if = "Option::is_none")]
 143 |     pub cert_issuer: Option<String>,
 144 | 
 145 |     #[serde(skip_serializing_if = "Option::is_none")]
 146 |     pub cert_url: Option<String>,
 147 | 
 148 |     #[serde(skip_serializing_if = "Option::is_none")]
 149 |     pub fulcio_certs: Option<PathBuf>,
 150 | 
 151 |     pub insecure_skip_signature: bool,
 152 | 
 153 |     #[serde(skip_serializing_if = "Option::is_none")]
 154 |     pub rekor_pub_keys: Option<PathBuf>,
 155 | 
 156 |     pub use_sigstore_tuf_data: bool,
 157 | }
 158 | 
 159 | impl Default for OciConfig {
 160 |     fn default() -> Self {
 161 |         OciConfig {
 162 |             cert_email: None,
 163 |             cert_issuer: None,
 164 |             cert_url: None,
 165 |             fulcio_certs: None,
 166 |             insecure_skip_signature: false,
 167 |             rekor_pub_keys: None,
 168 |             use_sigstore_tuf_data: true,
 169 |         }
 170 |     }
 171 | }
 172 | 
 173 | #[derive(Clone, Debug, Deserialize, Serialize)]
 174 | pub struct PluginConfig {
 175 |     #[serde(rename = "url", alias = "path")]
 176 |     pub url: Url,
 177 |     pub runtime_config: Option<RuntimeConfig>,
 178 | }
 179 | 
 180 | mod skip_serde {
 181 |     use super::*;
 182 |     use serde::{Deserializer, Serializer};
 183 | 
 184 |     pub fn serialize<S>(set: &Option<RegexSet>, serializer: S) -> Result<S::Ok, S::Error>
 185 |     where
 186 |         S: Serializer,
 187 |     {
 188 |         match set {
 189 |             Some(set) => serializer.serialize_some(set.patterns()),
 190 |             None => serializer.serialize_none(),
 191 |         }
 192 |     }
 193 | 
 194 |     fn anchor_pattern(pattern: &String) -> String {
 195 |         // Anchor the pattern to match the entire string
 196 |         // only if it is not already anchored
 197 |         if pattern.starts_with("^")
 198 |             || pattern.starts_with("\\A")
 199 |             || pattern.ends_with("$")
 200 |             || pattern.ends_with("\\z")
 201 |         {
 202 |             pattern.clone()
 203 |         } else {
 204 |             format!("^{}$", pattern)
 205 |         }
 206 |     }
 207 | 
 208 |     pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<RegexSet>, D::Error>
 209 |     where
 210 |         D: Deserializer<'de>,
 211 |     {
 212 |         let patterns: Option<Vec<String>> = Option::deserialize(deserializer)?;
 213 |         match patterns {
 214 |             Some(patterns) => RegexSet::new(
 215 |                 patterns
 216 |                     .into_iter()
 217 |                     .map(|p| anchor_pattern(&p))
 218 |                     .collect::<Vec<_>>(),
 219 |             )
 220 |             .map(Some)
 221 |             .map_err(serde::de::Error::custom),
 222 |             None => Ok(None),
 223 |         }
 224 |     }
 225 | }
 226 | 
 227 | #[derive(Clone, Debug, Default, Deserialize, Serialize)]
 228 | pub struct RuntimeConfig {
 229 |     // List of prompts to skip loading at runtime.
 230 |     #[serde(with = "skip_serde", default)]
 231 |     pub skip_prompts: Option<RegexSet>,
 232 |     // List of resource templatess to skip loading at runtime.
 233 |     #[serde(with = "skip_serde", default)]
 234 |     pub skip_resource_templates: Option<RegexSet>,
 235 |     // List of resources to skip loading at runtime.
 236 |     #[serde(with = "skip_serde", default)]
 237 |     pub skip_resources: Option<RegexSet>,
 238 |     // List of tools to skip loading at runtime.
 239 |     #[serde(with = "skip_serde", default)]
 240 |     pub skip_tools: Option<RegexSet>,
 241 |     pub allowed_hosts: Option<Vec<String>>,
 242 |     pub allowed_paths: Option<Vec<String>>,
 243 |     pub env_vars: Option<HashMap<String, String>>,
 244 |     pub memory_limit: Option<String>,
 245 | }
 246 | 
 247 | pub async fn load_config(cli: &Cli) -> Result<Config> {
 248 |     // Get default config path in the user's config directory
 249 |     let default_config_path = dirs::config_dir()
 250 |         .map(|mut path| {
 251 |             path.push("hyper-mcp");
 252 |             path.push("config.json");
 253 |             path
 254 |         })
 255 |         .unwrap();
 256 | 
 257 |     let config_path = cli.config_file.as_ref().unwrap_or(&default_config_path);
 258 |     if !config_path.exists() {
 259 |         return Err(anyhow::anyhow!(
 260 |             "Config file not found at: {}. Please create a config file first.",
 261 |             config_path.display()
 262 |         ));
 263 |     }
 264 |     tracing::info!("Using config file at {}", config_path.display());
 265 |     let ext = config_path
 266 |         .extension()
 267 |         .and_then(|e| e.to_str())
 268 |         .unwrap_or("");
 269 | 
 270 |     let content = tokio::fs::read_to_string(config_path)
 271 |         .await
 272 |         .with_context(|| format!("Failed to read config file at {}", config_path.display()))?;
 273 | 
 274 |     let mut config: Config = match ext {
 275 |         "json" => serde_json::from_str(&content)?,
 276 |         "yaml" | "yml" => serde_yaml::from_str(&content)?,
 277 |         "toml" => toml::from_str(&content)?,
 278 |         _ => return Err(anyhow::anyhow!("Unsupported config format: {ext}")),
 279 |     };
 280 | 
 281 |     let mut oci = config.oci.clone();
 282 | 
 283 |     if let Some(skip) = cli.insecure_skip_signature {
 284 |         oci.insecure_skip_signature = skip;
 285 |     }
 286 |     if let Some(use_tuf) = cli.use_sigstore_tuf_data {
 287 |         oci.use_sigstore_tuf_data = use_tuf;
 288 |     }
 289 |     if let Some(rekor_keys) = &cli.rekor_pub_keys {
 290 |         oci.rekor_pub_keys = Some(rekor_keys.clone());
 291 |     }
 292 |     if let Some(fulcio_certs) = &cli.fulcio_certs {
 293 |         oci.fulcio_certs = Some(fulcio_certs.clone());
 294 |     }
 295 |     if let Some(issuer) = &cli.cert_issuer {
 296 |         oci.cert_issuer = Some(issuer.clone());
 297 |     }
 298 |     if let Some(email) = &cli.cert_email {
 299 |         oci.cert_email = Some(email.clone());
 300 |     }
 301 |     if let Some(url) = &cli.cert_url {
 302 |         oci.cert_url = Some(url.clone());
 303 |     }
 304 |     config.oci = oci;
 305 | 
 306 |     Ok(config)
 307 | }
 308 | 
 309 | #[cfg(test)]
 310 | mod tests {
 311 |     use super::*;
 312 |     use std::path::Path;
 313 |     use tokio::runtime::Runtime;
 314 | 
 315 |     #[test]
 316 |     fn test_plugin_name_valid() {
 317 |         let valid_names = vec!["plugin1", "plugin_name", "PluginName", "plugin123"];
 318 | 
 319 |         for name in valid_names {
 320 |             assert!(
 321 |                 PluginName::try_from(name).is_ok(),
 322 |                 "Failed to parse valid name: {name}"
 323 |             );
 324 |         }
 325 |     }
 326 | 
 327 |     #[test]
 328 |     fn test_plugin_name_invalid_comprehensive() {
 329 |         // Test various hyphen scenarios - hyphens are no longer allowed
 330 |         let hyphen_cases = vec![
 331 |             ("plugin-name", "single hyphen"),
 332 |             ("plugin-name-test", "multiple hyphens"),
 333 |             ("-plugin", "leading hyphen"),
 334 |             ("plugin-", "trailing hyphen"),
 335 |             ("--plugin", "leading double hyphen"),
 336 |             ("plugin--", "trailing double hyphen"),
 337 |             ("plugin--name", "consecutive hyphens"),
 338 |             ("plugin-_name", "hyphen before underscore"),
 339 |             ("plugin_-name", "hyphen after underscore"),
 340 |             ("my-plugin-123", "hyphens with numbers"),
 341 |             ("Plugin-Name", "hyphens with capitals"),
 342 |         ];
 343 | 
 344 |         for (name, description) in hyphen_cases {
 345 |             assert!(
 346 |                 PluginName::try_from(name).is_err(),
 347 |                 "Should reject plugin name '{name}' ({description})"
 348 |             );
 349 |         }
 350 | 
 351 |         // Test underscore edge cases
 352 |         let underscore_cases = vec![
 353 |             ("_plugin", "leading underscore"),
 354 |             ("plugin_", "trailing underscore"),
 355 |             ("__plugin", "leading double underscore"),
 356 |             ("plugin__", "trailing double underscore"),
 357 |             ("plugin__name", "consecutive underscores"),
 358 |             ("_plugin_", "leading and trailing underscores"),
 359 |         ];
 360 | 
 361 |         for (name, description) in underscore_cases {
 362 |             assert!(
 363 |                 PluginName::try_from(name).is_err(),
 364 |                 "Should reject plugin name '{name}' ({description})"
 365 |             );
 366 |         }
 367 | 
 368 |         // Test special characters
 369 |         let special_char_cases = vec![
 370 |             ("plugin@name", "at symbol"),
 371 |             ("plugin#name", "hash symbol"),
 372 |             ("plugin$name", "dollar sign"),
 373 |             ("plugin%name", "percent sign"),
 374 |             ("plugin&name", "ampersand"),
 375 |             ("plugin*name", "asterisk"),
 376 |             ("plugin(name)", "parentheses"),
 377 |             ("plugin+name", "plus sign"),
 378 |             ("plugin=name", "equals sign"),
 379 |             ("plugin[name]", "square brackets"),
 380 |             ("plugin{name}", "curly braces"),
 381 |             ("plugin|name", "pipe symbol"),
 382 |             ("plugin\\name", "backslash"),
 383 |             ("plugin:name", "colon"),
 384 |             ("plugin;name", "semicolon"),
 385 |             ("plugin\"name", "double quote"),
 386 |             ("plugin'name", "single quote"),
 387 |             ("plugin<name>", "angle brackets"),
 388 |             ("plugin,name", "comma"),
 389 |             ("plugin.name", "period"),
 390 |             ("plugin/name", "forward slash"),
 391 |             ("plugin?name", "question mark"),
 392 |         ];
 393 | 
 394 |         for (name, description) in special_char_cases {
 395 |             assert!(
 396 |                 PluginName::try_from(name).is_err(),
 397 |                 "Should reject plugin name '{name}' ({description})"
 398 |             );
 399 |         }
 400 | 
 401 |         // Test whitespace cases
 402 |         let whitespace_cases = vec![
 403 |             ("plugin name", "space in middle"),
 404 |             (" plugin", "leading space"),
 405 |             ("plugin ", "trailing space"),
 406 |             ("  plugin", "leading double space"),
 407 |             ("plugin  ", "trailing double space"),
 408 |             ("plugin  name", "double space in middle"),
 409 |             ("plugin\tname", "tab character"),
 410 |             ("plugin\nname", "newline character"),
 411 |             ("plugin\rname", "carriage return"),
 412 |         ];
 413 | 
 414 |         for (name, description) in whitespace_cases {
 415 |             assert!(
 416 |                 PluginName::try_from(name).is_err(),
 417 |                 "Should reject plugin name '{name}' ({description})"
 418 |             );
 419 |         }
 420 | 
 421 |         // Test empty and minimal cases
 422 |         let empty_cases = vec![
 423 |             ("", "empty string"),
 424 |             ("_", "single underscore"),
 425 |             ("-", "single hyphen"),
 426 |             ("__", "double underscore"),
 427 |             ("--", "double hyphen"),
 428 |             ("_-", "underscore-hyphen"),
 429 |             ("-_", "hyphen-underscore"),
 430 |         ];
 431 | 
 432 |         for (name, description) in empty_cases {
 433 |             assert!(
 434 |                 PluginName::try_from(name).is_err(),
 435 |                 "Should reject plugin name '{name}' ({description})"
 436 |             );
 437 |         }
 438 | 
 439 |         // Test unicode and non-ASCII cases
 440 |         let unicode_cases = vec![
 441 |             ("plugín", "accented character"),
 442 |             ("plügïn", "umlaut characters"),
 443 |             ("плагин", "cyrillic characters"),
 444 |             ("プラグイン", "japanese characters"),
 445 |             ("插件", "chinese characters"),
 446 |             ("plugin名前", "mixed ASCII and japanese"),
 447 |             ("café-plugin", "accented character with hyphen"),
 448 |         ];
 449 | 
 450 |         for (name, description) in unicode_cases {
 451 |             assert!(
 452 |                 PluginName::try_from(name).is_err(),
 453 |                 "Should reject plugin name '{name}' ({description})"
 454 |             );
 455 |         }
 456 |     }
 457 | 
 458 |     #[test]
 459 |     fn test_plugin_name_valid_comprehensive() {
 460 |         // Test basic alphanumeric names
 461 |         let basic_cases = vec![
 462 |             ("plugin", "simple lowercase"),
 463 |             ("Plugin", "simple capitalized"),
 464 |             ("PLUGIN", "simple uppercase"),
 465 |             ("MyPlugin", "camelCase"),
 466 |             ("plugin123", "with numbers"),
 467 |             ("123plugin", "starting with numbers"),
 468 |             ("p", "single character"),
 469 |             ("P", "single uppercase character"),
 470 |             ("1", "single number"),
 471 |         ];
 472 | 
 473 |         for (name, description) in basic_cases {
 474 |             assert!(
 475 |                 PluginName::try_from(name).is_ok(),
 476 |                 "Should accept valid plugin name '{name}' ({description})"
 477 |             );
 478 |         }
 479 | 
 480 |         // Test names with underscores as separators
 481 |         let underscore_cases = vec![
 482 |             ("plugin_name", "simple underscore"),
 483 |             ("my_plugin", "underscore separator"),
 484 |             ("plugin_name_test", "multiple underscores"),
 485 |             ("Plugin_Name", "underscore with capitals"),
 486 |             ("plugin_123", "underscore with numbers"),
 487 |             ("my_plugin_v2", "complex with version"),
 488 |             ("a_b", "minimal underscore case"),
 489 |             ("test_plugin_name_123", "long with mixed content"),
 490 |         ];
 491 | 
 492 |         for (name, description) in underscore_cases {
 493 |             assert!(
 494 |                 PluginName::try_from(name).is_ok(),
 495 |                 "Should accept valid plugin name '{name}' ({description})"
 496 |             );
 497 |         }
 498 | 
 499 |         // Test mixed alphanumeric cases
 500 |         let mixed_cases = vec![
 501 |             ("plugin1", "letters and single digit"),
 502 |             ("plugin123", "letters and multiple digits"),
 503 |             ("Plugin1Name", "mixed case with digits"),
 504 |             ("myPlugin2", "camelCase with digit"),
 505 |             ("testPlugin123", "longer mixed case"),
 506 |             ("ABC123", "all caps with numbers"),
 507 |             ("plugin1_test2", "mixed with underscore"),
 508 |             ("My_Plugin_V123", "complex mixed case"),
 509 |         ];
 510 | 
 511 |         for (name, description) in mixed_cases {
 512 |             assert!(
 513 |                 PluginName::try_from(name).is_ok(),
 514 |                 "Should accept valid plugin name '{name}' ({description})"
 515 |             );
 516 |         }
 517 | 
 518 |         // Test longer valid names
 519 |         let longer_cases = vec![
 520 |             (
 521 |                 "very_long_plugin_name_that_should_be_valid",
 522 |                 "very long name",
 523 |             ),
 524 |             (
 525 |                 "plugin_with_many_underscores_and_numbers_123",
 526 |                 "long mixed content",
 527 |             ),
 528 |             ("MyVeryLongPluginNameThatShouldWork", "long camelCase"),
 529 |             ("VERY_LONG_UPPERCASE_PLUGIN_NAME", "long uppercase"),
 530 |         ];
 531 | 
 532 |         for (name, description) in longer_cases {
 533 |             assert!(
 534 |                 PluginName::try_from(name).is_ok(),
 535 |                 "Should accept valid plugin name '{name}' ({description})"
 536 |             );
 537 |         }
 538 | 
 539 |         // Test edge cases that should be valid
 540 |         let edge_cases = vec![
 541 |             ("a1", "minimal valid case"),
 542 |             ("1a", "number then letter"),
 543 |             ("a_1", "letter underscore number"),
 544 |             ("1_a", "number underscore letter"),
 545 |         ];
 546 | 
 547 |         for (name, description) in edge_cases {
 548 |             assert!(
 549 |                 PluginName::try_from(name).is_ok(),
 550 |                 "Should accept valid plugin name '{name}' ({description})"
 551 |             );
 552 |         }
 553 |     }
 554 | 
 555 |     #[test]
 556 |     fn test_plugin_name_display() {
 557 |         let name_str = "plugin_name_123";
 558 |         let plugin_name = PluginName::try_from(name_str).unwrap();
 559 |         assert_eq!(plugin_name.to_string(), name_str);
 560 |     }
 561 | 
 562 |     #[test]
 563 |     fn test_plugin_name_serialize_deserialize() {
 564 |         let name_str = "plugin_name_123";
 565 |         let plugin_name = PluginName::try_from(name_str).unwrap();
 566 | 
 567 |         // Serialize
 568 |         let serialized = serde_json::to_string(&plugin_name).unwrap();
 569 |         assert_eq!(serialized, format!("\"{name_str}\""));
 570 | 
 571 |         // Deserialize
 572 |         let deserialized: PluginName = serde_json::from_str(&serialized).unwrap();
 573 |         assert_eq!(deserialized, plugin_name);
 574 |     }
 575 | 
 576 |     #[test]
 577 |     fn test_load_valid_yaml_config() {
 578 |         let rt = Runtime::new().unwrap();
 579 | 
 580 |         // Read the test fixture file
 581 |         let path = Path::new("tests/fixtures/valid_config.yaml");
 582 | 
 583 |         let cli = Cli {
 584 |             config_file: Some(path.to_path_buf()),
 585 | 
 586 |             ..Default::default()
 587 |         };
 588 | 
 589 |         // Load the config
 590 |         let config_result = rt.block_on(load_config(&cli));
 591 |         assert!(config_result.is_ok(), "Failed to load valid YAML config");
 592 | 
 593 |         let config = config_result.unwrap();
 594 |         assert_eq!(config.plugins.len(), 3, "Expected 3 plugins in the config");
 595 | 
 596 |         // Verify plugin names
 597 |         assert!(
 598 |             config
 599 |                 .plugins
 600 |                 .contains_key(&PluginName("test_plugin".to_string()))
 601 |         );
 602 |         assert!(
 603 |             config
 604 |                 .plugins
 605 |                 .contains_key(&PluginName("another_plugin".to_string()))
 606 |         );
 607 |         assert!(
 608 |             config
 609 |                 .plugins
 610 |                 .contains_key(&PluginName("minimal_plugin".to_string()))
 611 |         );
 612 | 
 613 |         // Verify plugin configs
 614 |         let test_plugin = &config.plugins[&PluginName("test_plugin".to_string())];
 615 |         assert_eq!(test_plugin.url.to_string(), "file:///path/to/plugin");
 616 | 
 617 |         let runtime_config = test_plugin.runtime_config.as_ref().unwrap();
 618 |         assert_eq!(runtime_config.skip_tools.as_ref().unwrap().len(), 2);
 619 |         assert_eq!(runtime_config.allowed_hosts.as_ref().unwrap().len(), 2);
 620 |         assert_eq!(runtime_config.allowed_paths.as_ref().unwrap().len(), 2);
 621 |         assert_eq!(runtime_config.env_vars.as_ref().unwrap().len(), 2);
 622 |         assert_eq!(runtime_config.memory_limit.as_ref().unwrap(), "1GB");
 623 | 
 624 |         // Verify minimal plugin has no runtime config
 625 |         let minimal_plugin = &config.plugins[&PluginName("minimal_plugin".to_string())];
 626 |         assert!(minimal_plugin.runtime_config.is_none());
 627 |     }
 628 | 
 629 |     #[test]
 630 |     fn test_load_valid_json_config() {
 631 |         let rt = Runtime::new().unwrap();
 632 | 
 633 |         // Read the test fixture file
 634 |         let path = Path::new("tests/fixtures/valid_config.json");
 635 | 
 636 |         let cli = Cli {
 637 |             config_file: Some(path.to_path_buf()),
 638 | 
 639 |             ..Default::default()
 640 |         };
 641 | 
 642 |         // Load the config
 643 |         let config_result = rt.block_on(load_config(&cli));
 644 | 
 645 |         assert!(config_result.is_ok(), "Failed to load valid JSON config");
 646 | 
 647 |         let config = config_result.unwrap();
 648 |         assert_eq!(config.plugins.len(), 3, "Expected 3 plugins in the config");
 649 | 
 650 |         // Verify plugin names
 651 |         assert!(
 652 |             config
 653 |                 .plugins
 654 |                 .contains_key(&PluginName("test_plugin".to_string()))
 655 |         );
 656 |         assert!(
 657 |             config
 658 |                 .plugins
 659 |                 .contains_key(&PluginName("another_plugin".to_string()))
 660 |         );
 661 |         assert!(
 662 |             config
 663 |                 .plugins
 664 |                 .contains_key(&PluginName("minimal_plugin".to_string()))
 665 |         );
 666 | 
 667 |         // Verify env vars
 668 |         let test_plugin = &config.plugins[&PluginName("test_plugin".to_string())];
 669 |         let runtime_config = test_plugin.runtime_config.as_ref().unwrap();
 670 |         assert_eq!(runtime_config.env_vars.as_ref().unwrap()["DEBUG"], "true");
 671 |         assert_eq!(
 672 |             runtime_config.env_vars.as_ref().unwrap()["LOG_LEVEL"],
 673 |             "info"
 674 |         );
 675 |     }
 676 | 
 677 |     #[test]
 678 |     fn test_load_invalid_plugin_name() {
 679 |         let rt = Runtime::new().unwrap();
 680 | 
 681 |         // Read the test fixture file
 682 |         let path = Path::new("tests/fixtures/invalid_plugin_name.yaml");
 683 | 
 684 |         let cli = Cli {
 685 |             config_file: Some(path.to_path_buf()),
 686 | 
 687 |             ..Default::default()
 688 |         };
 689 | 
 690 |         // Load the config
 691 |         let config_result = rt.block_on(load_config(&cli));
 692 |         assert!(
 693 |             config_result.is_err(),
 694 |             "Expected error for invalid plugin name"
 695 |         );
 696 |     }
 697 | 
 698 |     #[test]
 699 |     fn test_load_invalid_url() {
 700 |         let rt = Runtime::new().unwrap();
 701 | 
 702 |         // Read the test fixture file
 703 |         let path = Path::new("tests/fixtures/invalid_url.yaml");
 704 | 
 705 |         let cli = Cli {
 706 |             config_file: Some(path.to_path_buf()),
 707 | 
 708 |             ..Default::default()
 709 |         };
 710 | 
 711 |         // Load the config
 712 |         let config_result = rt.block_on(load_config(&cli));
 713 |         assert!(config_result.is_err(), "Expected error for invalid URL");
 714 | 
 715 |         let error = config_result.unwrap_err();
 716 |         assert!(
 717 |             error.to_string().contains("not a valid url")
 718 |                 || error.to_string().contains("invalid URL"),
 719 |             "Error should mention the invalid URL"
 720 |         );
 721 |     }
 722 | 
 723 |     #[test]
 724 |     fn test_load_invalid_structure() {
 725 |         let rt = Runtime::new().unwrap();
 726 | 
 727 |         // Read the test fixture file
 728 |         let path = Path::new("tests/fixtures/invalid_structure.yaml");
 729 | 
 730 |         let cli = Cli {
 731 |             config_file: Some(path.to_path_buf()),
 732 | 
 733 |             ..Default::default()
 734 |         };
 735 | 
 736 |         // Load the config
 737 |         let config_result = rt.block_on(load_config(&cli));
 738 |         assert!(
 739 |             config_result.is_err(),
 740 |             "Expected error for invalid structure"
 741 |         );
 742 |     }
 743 | 
 744 |     #[test]
 745 |     fn test_load_nonexistent_file() {
 746 |         let rt = Runtime::new().unwrap();
 747 | 
 748 |         // Create a path that doesn't exist
 749 |         let nonexistent_path = Path::new("/tmp/definitely_not_a_real_config_file_12345.yaml");
 750 | 
 751 |         let cli = Cli {
 752 |             config_file: Some(nonexistent_path.to_path_buf()),
 753 | 
 754 |             ..Default::default()
 755 |         };
 756 | 
 757 |         // Load the config
 758 |         let config_result = rt.block_on(load_config(&cli));
 759 |         assert!(
 760 |             config_result.is_err(),
 761 |             "Expected error for nonexistent file"
 762 |         );
 763 | 
 764 |         let error = config_result.unwrap_err();
 765 |         assert!(
 766 |             error.to_string().contains("not found"),
 767 |             "Error should mention file not found"
 768 |         );
 769 |     }
 770 | 
 771 |     #[test]
 772 |     fn test_load_unsupported_extension() {
 773 |         let rt = Runtime::new().unwrap();
 774 | 
 775 |         let path = Path::new("tests/fixtures/unsupported_config.txt");
 776 | 
 777 |         let cli = Cli {
 778 |             config_file: Some(path.to_path_buf()),
 779 | 
 780 |             ..Default::default()
 781 |         };
 782 | 
 783 |         // Load the config
 784 |         let config_result = rt.block_on(load_config(&cli));
 785 |         assert!(
 786 |             config_result.is_err(),
 787 |             "Expected error for unsupported extension"
 788 |         );
 789 | 
 790 |         let error = config_result.unwrap_err();
 791 |         assert!(
 792 |             error.to_string().contains("Unsupported config format"),
 793 |             "Error should mention unsupported format"
 794 |         );
 795 |     }
 796 | 
 797 |     #[test]
 798 |     fn test_auth_config_basic_serialization() {
 799 |         let auth_config = AuthConfig::Basic {
 800 |             username: "testuser".to_string(),
 801 |             password: "testpass".to_string(),
 802 |         };
 803 | 
 804 |         let serialized = serde_json::to_string(&auth_config).unwrap();
 805 |         let expected = r#"{"type":"basic","username":"testuser","password":"testpass"}"#;
 806 |         assert_eq!(serialized, expected);
 807 |     }
 808 | 
 809 |     #[test]
 810 |     fn test_auth_config_token_serialization() {
 811 |         let auth_config = AuthConfig::Token {
 812 |             token: "test-token-123".to_string(),
 813 |         };
 814 | 
 815 |         let serialized = serde_json::to_string(&auth_config).unwrap();
 816 |         let expected = r#"{"type":"token","token":"test-token-123"}"#;
 817 |         assert_eq!(serialized, expected);
 818 |     }
 819 | 
 820 |     #[test]
 821 |     fn test_auth_config_basic_deserialization() {
 822 |         let json = r#"{"type":"basic","username":"testuser","password":"testpass"}"#;
 823 |         let auth_config: AuthConfig = serde_json::from_str(json).unwrap();
 824 | 
 825 |         match auth_config {
 826 |             AuthConfig::Basic { username, password } => {
 827 |                 assert_eq!(username, "testuser");
 828 |                 assert_eq!(password, "testpass");
 829 |             }
 830 |             _ => panic!("Expected Basic auth config"),
 831 |         }
 832 |     }
 833 | 
 834 |     #[test]
 835 |     fn test_auth_config_token_deserialization() {
 836 |         let json = r#"{"type":"token","token":"test-token-123"}"#;
 837 |         let auth_config: AuthConfig = serde_json::from_str(json).unwrap();
 838 | 
 839 |         match auth_config {
 840 |             AuthConfig::Token { token } => {
 841 |                 assert_eq!(token, "test-token-123");
 842 |             }
 843 |             _ => panic!("Expected Token auth config"),
 844 |         }
 845 |     }
 846 | 
 847 |     #[test]
 848 |     fn test_auth_config_yaml_basic_deserialization() {
 849 |         let yaml = r#"
 850 | type: basic
 851 | username: testuser
 852 | password: testpass
 853 | "#;
 854 |         let auth_config: AuthConfig = serde_yaml::from_str(yaml).unwrap();
 855 | 
 856 |         match auth_config {
 857 |             AuthConfig::Basic { username, password } => {
 858 |                 assert_eq!(username, "testuser");
 859 |                 assert_eq!(password, "testpass");
 860 |             }
 861 |             _ => panic!("Expected Basic auth config"),
 862 |         }
 863 |     }
 864 | 
 865 |     #[test]
 866 |     fn test_auth_config_yaml_token_deserialization() {
 867 |         let yaml = r#"
 868 | type: token
 869 | token: test-token-123
 870 | "#;
 871 |         let auth_config: AuthConfig = serde_yaml::from_str(yaml).unwrap();
 872 | 
 873 |         match auth_config {
 874 |             AuthConfig::Token { token } => {
 875 |                 assert_eq!(token, "test-token-123");
 876 |             }
 877 |             _ => panic!("Expected Token auth config"),
 878 |         }
 879 |     }
 880 | 
 881 |     #[test]
 882 |     fn test_auth_config_invalid_type() {
 883 |         let json = r#"{"type":"invalid","data":"test"}"#;
 884 |         let result: Result<AuthConfig, _> = serde_json::from_str(json);
 885 |         assert!(result.is_err(), "Expected error for invalid auth type");
 886 |     }
 887 | 
 888 |     #[test]
 889 |     fn test_auth_config_missing_fields() {
 890 |         // Missing username for basic auth
 891 |         let json = r#"{"type":"basic","password":"testpass"}"#;
 892 |         let result: Result<AuthConfig, _> = serde_json::from_str(json);
 893 |         assert!(result.is_err(), "Expected error for missing username");
 894 | 
 895 |         // Missing password for basic auth
 896 |         let json = r#"{"type":"basic","username":"testuser"}"#;
 897 |         let result: Result<AuthConfig, _> = serde_json::from_str(json);
 898 |         assert!(result.is_err(), "Expected error for missing password");
 899 | 
 900 |         // Missing token for token auth
 901 |         let json = r#"{"type":"token"}"#;
 902 |         let result: Result<AuthConfig, _> = serde_json::from_str(json);
 903 |         assert!(result.is_err(), "Expected error for missing token");
 904 |     }
 905 | 
 906 |     #[test]
 907 |     fn test_config_with_auths_deserialization() {
 908 |         let json = r#"
 909 | {
 910 |   "auths": {
 911 |     "https://api.example.com": {
 912 |       "type": "basic",
 913 |       "username": "testuser",
 914 |       "password": "testpass"
 915 |     },
 916 |     "https://secure.api.com": {
 917 |       "type": "token",
 918 |       "token": "bearer-token-123"
 919 |     }
 920 |   },
 921 |   "plugins": {
 922 |     "test_plugin": {
 923 |       "url": "file:///path/to/plugin"
 924 |     }
 925 |   }
 926 | }
 927 | "#;
 928 | 
 929 |         let config: Config = serde_json::from_str(json).unwrap();
 930 |         assert!(config.auths.is_some());
 931 | 
 932 |         let auths = config.auths.unwrap();
 933 |         assert_eq!(auths.len(), 2);
 934 | 
 935 |         let api_url = Url::parse("https://api.example.com").unwrap();
 936 |         let secure_url = Url::parse("https://secure.api.com").unwrap();
 937 | 
 938 |         assert!(auths.contains_key(&api_url));
 939 |         assert!(auths.contains_key(&secure_url));
 940 | 
 941 |         match &auths[&api_url] {
 942 |             AuthConfig::Basic { username, password } => {
 943 |                 assert_eq!(username, "testuser");
 944 |                 assert_eq!(password, "testpass");
 945 |             }
 946 |             _ => panic!("Expected Basic auth for api.example.com"),
 947 |         }
 948 | 
 949 |         match &auths[&secure_url] {
 950 |             AuthConfig::Token { token } => {
 951 |                 assert_eq!(token, "bearer-token-123");
 952 |             }
 953 |             _ => panic!("Expected Token auth for secure.api.com"),
 954 |         }
 955 |     }
 956 | 
 957 |     #[test]
 958 |     fn test_config_with_auths_yaml_deserialization() {
 959 |         let yaml = r#"
 960 | auths:
 961 |   "https://api.example.com":
 962 |     type: basic
 963 |     username: testuser
 964 |     password: testpass
 965 |   "https://secure.api.com":
 966 |     type: token
 967 |     token: bearer-token-123
 968 | plugins:
 969 |   test_plugin:
 970 |     url: "file:///path/to/plugin"
 971 | "#;
 972 | 
 973 |         let config: Config = serde_yaml::from_str(yaml).unwrap();
 974 |         assert!(config.auths.is_some());
 975 | 
 976 |         let auths = config.auths.unwrap();
 977 |         assert_eq!(auths.len(), 2);
 978 | 
 979 |         let api_url = Url::parse("https://api.example.com").unwrap();
 980 |         let secure_url = Url::parse("https://secure.api.com").unwrap();
 981 | 
 982 |         assert!(auths.contains_key(&api_url));
 983 |         assert!(auths.contains_key(&secure_url));
 984 |     }
 985 | 
 986 |     #[test]
 987 |     fn test_config_without_auths() {
 988 |         let json = r#"
 989 | {
 990 |   "plugins": {
 991 |     "test_plugin": {
 992 |       "url": "file:///path/to/plugin"
 993 |     }
 994 |   }
 995 | }
 996 | "#;
 997 | 
 998 |         let config: Config = serde_json::from_str(json).unwrap();
 999 |         assert!(config.auths.is_none());
1000 |         assert_eq!(config.plugins.len(), 1);
1001 |     }
1002 | 
1003 |     #[test]
1004 |     fn test_auth_config_clone() {
1005 |         let auth_config = AuthConfig::Basic {
1006 |             username: "testuser".to_string(),
1007 |             password: "testpass".to_string(),
1008 |         };
1009 | 
1010 |         let cloned = auth_config.clone();
1011 |         match cloned {
1012 |             AuthConfig::Basic { username, password } => {
1013 |                 assert_eq!(username, "testuser");
1014 |                 assert_eq!(password, "testpass");
1015 |             }
1016 |             _ => panic!("Expected Basic auth config"),
1017 |         }
1018 |     }
1019 | 
1020 |     #[test]
1021 |     fn test_auth_config_debug_format() {
1022 |         let auth_config = AuthConfig::Token {
1023 |             token: "secret-token".to_string(),
1024 |         };
1025 | 
1026 |         let debug_str = format!("{auth_config:?}");
1027 |         assert!(debug_str.contains("Token"));
1028 |         assert!(debug_str.contains("secret-token"));
1029 |     }
1030 | 
1031 |     #[test]
1032 |     fn test_internal_auth_config_keyring_deserialization() {
1033 |         let json = r#"{"type":"keyring","service":"test-service","user":"test-user"}"#;
1034 |         let result: Result<InternalAuthConfig, _> = serde_json::from_str(json);
1035 | 
1036 |         // This should deserialize successfully as InternalAuthConfig
1037 |         assert!(result.is_ok());
1038 | 
1039 |         match result.unwrap() {
1040 |             InternalAuthConfig::Keyring { service, user } => {
1041 |                 assert_eq!(service, "test-service");
1042 |                 assert_eq!(user, "test-user");
1043 |             }
1044 |             _ => panic!("Expected Keyring auth config"),
1045 |         }
1046 |     }
1047 | 
1048 |     #[test]
1049 |     fn test_auth_config_empty_values() {
1050 |         // Test with empty username
1051 |         let json = r#"{"type":"basic","username":"","password":"testpass"}"#;
1052 |         let auth_config: AuthConfig = serde_json::from_str(json).unwrap();
1053 |         match auth_config {
1054 |             AuthConfig::Basic { username, password } => {
1055 |                 assert_eq!(username, "");
1056 |                 assert_eq!(password, "testpass");
1057 |             }
1058 |             _ => panic!("Expected Basic auth config"),
1059 |         }
1060 | 
1061 |         // Test with empty token
1062 |         let json = r#"{"type":"token","token":""}"#;
1063 |         let auth_config: AuthConfig = serde_json::from_str(json).unwrap();
1064 |         match auth_config {
1065 |             AuthConfig::Token { token } => {
1066 |                 assert_eq!(token, "");
1067 |             }
1068 |             _ => panic!("Expected Token auth config"),
1069 |         }
1070 |     }
1071 | 
1072 |     #[test]
1073 |     fn test_load_config_with_auths_yaml() {
1074 |         let rt = Runtime::new().unwrap();
1075 |         let path = Path::new("tests/fixtures/config_with_auths.yaml");
1076 | 
1077 |         let cli = Cli {
1078 |             config_file: Some(path.to_path_buf()),
1079 | 
1080 |             ..Default::default()
1081 |         };
1082 | 
1083 |         let config_result = rt.block_on(load_config(&cli));
1084 |         assert!(
1085 |             config_result.is_ok(),
1086 |             "Failed to load config with auths from YAML"
1087 |         );
1088 | 
1089 |         let config = config_result.unwrap();
1090 |         assert!(config.auths.is_some(), "Expected auths to be present");
1091 | 
1092 |         let auths = config.auths.unwrap();
1093 |         assert_eq!(auths.len(), 4, "Expected 4 auth configurations");
1094 | 
1095 |         // Test basic auth
1096 |         let api_url = Url::parse("https://api.example.com").unwrap();
1097 |         assert!(auths.contains_key(&api_url));
1098 |         match &auths[&api_url] {
1099 |             AuthConfig::Basic { username, password } => {
1100 |                 assert_eq!(username, "testuser");
1101 |                 assert_eq!(password, "testpass");
1102 |             }
1103 |             _ => panic!("Expected Basic auth for api.example.com"),
1104 |         }
1105 | 
1106 |         // Test token auth
1107 |         let secure_url = Url::parse("https://secure.api.com").unwrap();
1108 |         assert!(auths.contains_key(&secure_url));
1109 |         match &auths[&secure_url] {
1110 |             AuthConfig::Token { token } => {
1111 |                 assert_eq!(token, "bearer-token-123");
1112 |             }
1113 |             _ => panic!("Expected Token auth for secure.api.com"),
1114 |         }
1115 |     }
1116 | 
1117 |     #[test]
1118 |     fn test_load_config_with_auths_json() {
1119 |         let rt = Runtime::new().unwrap();
1120 |         let path = Path::new("tests/fixtures/config_with_auths.json");
1121 | 
1122 |         let cli = Cli {
1123 |             config_file: Some(path.to_path_buf()),
1124 | 
1125 |             ..Default::default()
1126 |         };
1127 | 
1128 |         let config_result = rt.block_on(load_config(&cli));
1129 |         assert!(
1130 |             config_result.is_ok(),
1131 |             "Failed to load config with auths from JSON"
1132 |         );
1133 | 
1134 |         let config = config_result.unwrap();
1135 |         assert!(config.auths.is_some(), "Expected auths to be present");
1136 | 
1137 |         let auths = config.auths.unwrap();
1138 |         assert_eq!(auths.len(), 4, "Expected 4 auth configurations");
1139 | 
1140 |         // Test that all URLs are present
1141 |         let expected_urls = vec![
1142 |             "https://api.example.com",
1143 |             "https://secure.api.com",
1144 |             "https://private.registry.io",
1145 |             "https://oauth.service.com",
1146 |         ];
1147 | 
1148 |         for url_str in expected_urls {
1149 |             let url = Url::parse(url_str).unwrap();
1150 |             assert!(auths.contains_key(&url), "Missing auth for {url_str}");
1151 |         }
1152 |     }
1153 | 
1154 |     #[test]
1155 |     fn test_load_invalid_auth_config() {
1156 |         let rt = Runtime::new().unwrap();
1157 |         let path = Path::new("tests/fixtures/invalid_auth_config.yaml");
1158 | 
1159 |         let cli = Cli {
1160 |             config_file: Some(path.to_path_buf()),
1161 | 
1162 |             ..Default::default()
1163 |         };
1164 | 
1165 |         let config_result = rt.block_on(load_config(&cli));
1166 |         assert!(
1167 |             config_result.is_err(),
1168 |             "Expected error for invalid auth config"
1169 |         );
1170 | 
1171 |         let error = config_result.unwrap_err();
1172 |         let error_msg = error.to_string();
1173 |         // The error should be related to deserialization
1174 |         assert!(
1175 |             error_msg.contains("unknown variant")
1176 |                 || error_msg.contains("missing field")
1177 |                 || error_msg.contains("invalid"),
1178 |             "Error should indicate invalid auth configuration: {error_msg}"
1179 |         );
1180 |     }
1181 | 
1182 |     #[test]
1183 |     fn test_auth_config_url_matching() {
1184 |         let mut auths = HashMap::new();
1185 | 
1186 |         // Add auth for specific API endpoint
1187 |         let api_url = Url::parse("https://api.example.com").unwrap();
1188 |         auths.insert(
1189 |             api_url,
1190 |             AuthConfig::Token {
1191 |                 token: "api-token".to_string(),
1192 |             },
1193 |         );
1194 | 
1195 |         // Add auth for broader domain
1196 |         let domain_url = Url::parse("https://example.com").unwrap();
1197 |         auths.insert(
1198 |             domain_url,
1199 |             AuthConfig::Basic {
1200 |                 username: "user".to_string(),
1201 |                 password: "pass".to_string(),
1202 |             },
1203 |         );
1204 | 
1205 |         let config = Config {
1206 |             auths: Some(auths),
1207 |             plugins: HashMap::new(),
1208 | 
1209 |             ..Default::default()
1210 |         };
1211 | 
1212 |         // Serialize and deserialize to test round-trip
1213 |         let json = serde_json::to_string(&config).unwrap();
1214 |         let deserialized: Config = serde_json::from_str(&json).unwrap();
1215 | 
1216 |         assert!(deserialized.auths.is_some());
1217 |         assert_eq!(deserialized.auths.unwrap().len(), 2);
1218 |     }
1219 | 
1220 |     #[test]
1221 |     fn test_auth_config_special_characters() {
1222 |         // Test with special characters in passwords and tokens
1223 |         let auth_basic = AuthConfig::Basic {
1224 |             username: "[email protected]".to_string(),
1225 |             password: "p@ssw0rd!#$%".to_string(),
1226 |         };
1227 | 
1228 |         let auth_token = AuthConfig::Token {
1229 |             token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ".to_string(),
1230 |         };
1231 | 
1232 |         // Test serialization
1233 |         let basic_json = serde_json::to_string(&auth_basic).unwrap();
1234 |         let token_json = serde_json::to_string(&auth_token).unwrap();
1235 | 
1236 |         // Test deserialization
1237 |         let basic_deserialized: AuthConfig = serde_json::from_str(&basic_json).unwrap();
1238 |         let token_deserialized: AuthConfig = serde_json::from_str(&token_json).unwrap();
1239 | 
1240 |         match basic_deserialized {
1241 |             AuthConfig::Basic { username, password } => {
1242 |                 assert_eq!(username, "[email protected]");
1243 |                 assert_eq!(password, "p@ssw0rd!#$%");
1244 |             }
1245 |             _ => panic!("Expected Basic auth config"),
1246 |         }
1247 | 
1248 |         match token_deserialized {
1249 |             AuthConfig::Token { token } => {
1250 |                 assert!(token.starts_with("eyJ"));
1251 |             }
1252 |             _ => panic!("Expected Token auth config"),
1253 |         }
1254 |     }
1255 | 
1256 |     #[test]
1257 |     fn test_config_auths_optional() {
1258 |         // Test config without auths field
1259 |         let json_without_auths = r#"
1260 | {
1261 |   "plugins": {
1262 |     "test_plugin": {
1263 |       "url": "file:///path/to/plugin"
1264 |     }
1265 |   }
1266 | }
1267 | "#;
1268 | 
1269 |         let config: Config = serde_json::from_str(json_without_auths).unwrap();
1270 |         assert!(config.auths.is_none());
1271 | 
1272 |         // Test config with empty auths
1273 |         let json_empty_auths = r#"
1274 | {
1275 |   "auths": {},
1276 |   "plugins": {
1277 |     "test_plugin": {
1278 |       "url": "file:///path/to/plugin"
1279 |     }
1280 |   }
1281 | }
1282 | "#;
1283 | 
1284 |         let config: Config = serde_json::from_str(json_empty_auths).unwrap();
1285 |         assert!(config.auths.is_some());
1286 |         assert_eq!(config.auths.unwrap().len(), 0);
1287 |     }
1288 | 
1289 |     #[test]
1290 |     fn test_keyring_auth_config_deserialization() {
1291 |         // Test that keyring config deserializes correctly as InternalAuthConfig
1292 |         let json = r#"{"type":"keyring","service":"test-service","user":"test-user"}"#;
1293 |         let internal_auth: InternalAuthConfig = serde_json::from_str(json).unwrap();
1294 | 
1295 |         match internal_auth {
1296 |             InternalAuthConfig::Keyring { service, user } => {
1297 |                 assert_eq!(service, "test-service");
1298 |                 assert_eq!(user, "test-user");
1299 |             }
1300 |             _ => panic!("Expected Keyring auth config"),
1301 |         }
1302 |     }
1303 | 
1304 |     #[test]
1305 |     fn test_documentation_example_yaml() {
1306 |         let rt = Runtime::new().unwrap();
1307 |         let path = Path::new("tests/fixtures/documentation_example.yaml");
1308 | 
1309 |         let cli = Cli {
1310 |             config_file: Some(path.to_path_buf()),
1311 | 
1312 |             ..Default::default()
1313 |         };
1314 | 
1315 |         let config_result = rt.block_on(load_config(&cli));
1316 |         assert!(
1317 |             config_result.is_ok(),
1318 |             "Documentation YAML example should be valid"
1319 |         );
1320 | 
1321 |         let config = config_result.unwrap();
1322 | 
1323 |         // Verify auths are present and correct
1324 |         assert!(config.auths.is_some());
1325 |         let auths = config.auths.unwrap();
1326 |         assert_eq!(
1327 |             auths.len(),
1328 |             3,
1329 |             "Expected 3 auth configurations from documentation example"
1330 |         );
1331 | 
1332 |         // Verify basic auth
1333 |         let registry_url = Url::parse("https://private.registry.io").unwrap();
1334 |         match &auths[&registry_url] {
1335 |             AuthConfig::Basic { username, password } => {
1336 |                 assert_eq!(username, "registry-user");
1337 |                 assert_eq!(password, "registry-pass");
1338 |             }
1339 |             _ => panic!("Expected Basic auth for private.registry.io"),
1340 |         }
1341 | 
1342 |         // Verify token auth
1343 |         let github_url = Url::parse("https://api.github.com").unwrap();
1344 |         match &auths[&github_url] {
1345 |             AuthConfig::Token { token } => {
1346 |                 assert_eq!(token, "ghp_1234567890abcdef");
1347 |             }
1348 |             _ => panic!("Expected Token auth for api.github.com"),
1349 |         }
1350 | 
1351 |         // Verify plugins
1352 |         assert_eq!(
1353 |             config.plugins.len(),
1354 |             3,
1355 |             "Expected 3 plugins from documentation example"
1356 |         );
1357 |         assert!(config.plugins.contains_key(&PluginName("time".to_string())));
1358 |         assert!(config.plugins.contains_key(&PluginName("myip".to_string())));
1359 |         assert!(
1360 |             config
1361 |                 .plugins
1362 |                 .contains_key(&PluginName("private_plugin".to_string()))
1363 |         );
1364 | 
1365 |         // Verify private plugin config
1366 |         let private_plugin = &config.plugins[&PluginName("private_plugin".to_string())];
1367 |         assert_eq!(
1368 |             private_plugin.url.to_string(),
1369 |             "https://private.registry.io/my_plugin"
1370 |         );
1371 |         assert!(private_plugin.runtime_config.is_some());
1372 |     }
1373 | 
1374 |     #[test]
1375 |     fn test_documentation_example_json() {
1376 |         let rt = Runtime::new().unwrap();
1377 |         let path = Path::new("tests/fixtures/documentation_example.json");
1378 | 
1379 |         let cli = Cli {
1380 |             config_file: Some(path.to_path_buf()),
1381 | 
1382 |             ..Default::default()
1383 |         };
1384 | 
1385 |         let config_result = rt.block_on(load_config(&cli));
1386 |         assert!(
1387 |             config_result.is_ok(),
1388 |             "Documentation JSON example should be valid"
1389 |         );
1390 | 
1391 |         let config = config_result.unwrap();
1392 | 
1393 |         // Verify auths are present and correct
1394 |         assert!(config.auths.is_some());
1395 |         let auths = config.auths.unwrap();
1396 |         assert_eq!(
1397 |             auths.len(),
1398 |             3,
1399 |             "Expected 3 auth configurations from documentation example"
1400 |         );
1401 | 
1402 |         // Verify all auth URLs are present
1403 |         let expected_auth_urls = vec![
1404 |             "https://private.registry.io",
1405 |             "https://api.github.com",
1406 |             "https://enterprise.api.com",
1407 |         ];
1408 | 
1409 |         for url_str in expected_auth_urls {
1410 |             let url = Url::parse(url_str).unwrap();
1411 |             assert!(auths.contains_key(&url), "Missing auth for {url_str}");
1412 |         }
1413 | 
1414 |         // Verify plugins match the documentation
1415 |         assert_eq!(config.plugins.len(), 3);
1416 | 
1417 |         let myip_plugin = &config.plugins[&PluginName("myip".to_string())];
1418 |         let runtime_config = myip_plugin.runtime_config.as_ref().unwrap();
1419 |         assert_eq!(runtime_config.env_vars.as_ref().unwrap()["FOO"], "bar");
1420 |         assert_eq!(runtime_config.memory_limit.as_ref().unwrap(), "512Mi");
1421 |     }
1422 | 
1423 |     #[test]
1424 |     fn test_url_prefix_matching_from_documentation() {
1425 |         // Test the URL matching behavior described in documentation
1426 |         let yaml = r#"
1427 | auths:
1428 |   "https://example.com":
1429 |     type: basic
1430 |     username: "broad-user"
1431 |     password: "broad-pass"
1432 |   "https://example.com/api":
1433 |     type: token
1434 |     token: "api-token"
1435 |   "https://example.com/api/v1":
1436 |     type: basic
1437 |     username: "v1-user"
1438 |     password: "v1-pass"
1439 | plugins:
1440 |   test_plugin:
1441 |     url: "file:///test"
1442 | "#;
1443 | 
1444 |         let config: Config = serde_yaml::from_str(yaml).unwrap();
1445 |         assert!(config.auths.is_some());
1446 | 
1447 |         let auths = config.auths.unwrap();
1448 |         assert_eq!(auths.len(), 3);
1449 | 
1450 |         // Verify all three auth configs are present
1451 |         let base_url = Url::parse("https://example.com").unwrap();
1452 |         let api_url = Url::parse("https://example.com/api").unwrap();
1453 |         let v1_url = Url::parse("https://example.com/api/v1").unwrap();
1454 | 
1455 |         assert!(auths.contains_key(&base_url));
1456 |         assert!(auths.contains_key(&api_url));
1457 |         assert!(auths.contains_key(&v1_url));
1458 | 
1459 |         // Verify the specific auth types match documentation
1460 |         match &auths[&base_url] {
1461 |             AuthConfig::Basic { username, .. } => {
1462 |                 assert_eq!(username, "broad-user");
1463 |             }
1464 |             _ => panic!("Expected Basic auth for base URL"),
1465 |         }
1466 | 
1467 |         match &auths[&api_url] {
1468 |             AuthConfig::Token { token } => {
1469 |                 assert_eq!(token, "api-token");
1470 |             }
1471 |             _ => panic!("Expected Token auth for API URL"),
1472 |         }
1473 | 
1474 |         match &auths[&v1_url] {
1475 |             AuthConfig::Basic { username, .. } => {
1476 |                 assert_eq!(username, "v1-user");
1477 |             }
1478 |             _ => panic!("Expected Basic auth for v1 URL"),
1479 |         }
1480 |     }
1481 | 
1482 |     #[test]
1483 |     fn test_keyring_json_format_validation() {
1484 |         // Test that the JSON formats shown in keyring documentation examples are valid
1485 | 
1486 |         // Test basic auth JSON format from documentation
1487 |         let basic_json = r#"{"type":"basic","username":"actual-user","password":"actual-pass"}"#;
1488 |         let basic_auth: AuthConfig = serde_json::from_str(basic_json).unwrap();
1489 | 
1490 |         match basic_auth {
1491 |             AuthConfig::Basic { username, password } => {
1492 |                 assert_eq!(username, "actual-user");
1493 |                 assert_eq!(password, "actual-pass");
1494 |             }
1495 |             _ => panic!("Expected Basic auth config from keyring JSON"),
1496 |         }
1497 | 
1498 |         // Test token auth JSON format from documentation
1499 |         let token_json = r#"{"type":"token","token":"actual-bearer-token"}"#;
1500 |         let token_auth: AuthConfig = serde_json::from_str(token_json).unwrap();
1501 | 
1502 |         match token_auth {
1503 |             AuthConfig::Token { token } => {
1504 |                 assert_eq!(token, "actual-bearer-token");
1505 |             }
1506 |             _ => panic!("Expected Token auth config from keyring JSON"),
1507 |         }
1508 | 
1509 |         // Test JWT-like token from documentation
1510 |         let jwt_json = r#"{"type":"token","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"}"#;
1511 |         let jwt_auth: AuthConfig = serde_json::from_str(jwt_json).unwrap();
1512 | 
1513 |         match jwt_auth {
1514 |             AuthConfig::Token { token } => {
1515 |                 assert_eq!(token, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
1516 |             }
1517 |             _ => panic!("Expected Token auth config from keyring JWT JSON"),
1518 |         }
1519 | 
1520 |         // Test corporate example from documentation
1521 |         let corp_json = r#"{"type":"basic","username":"corp_user","password":"corp_secret"}"#;
1522 |         let corp_auth: AuthConfig = serde_json::from_str(corp_json).unwrap();
1523 | 
1524 |         match corp_auth {
1525 |             AuthConfig::Basic { username, password } => {
1526 |                 assert_eq!(username, "corp_user");
1527 |                 assert_eq!(password, "corp_secret");
1528 |             }
1529 |             _ => panic!("Expected Basic auth config from corporate JSON"),
1530 |         }
1531 |     }
1532 | 
1533 |     #[test]
1534 |     #[ignore] // Requires system keyring access - run with `cargo test -- --ignored`
1535 |     fn test_keyring_auth_integration() {
1536 |         use std::process::Command;
1537 |         use std::time::{SystemTime, UNIX_EPOCH};
1538 | 
1539 |         // Generate unique service and user names to avoid conflicts
1540 |         let timestamp = SystemTime::now()
1541 |             .duration_since(UNIX_EPOCH)
1542 |             .unwrap()
1543 |             .as_secs();
1544 |         let service_name = format!("hyper-mcp-test-{timestamp}");
1545 |         let user_name = format!("test-user-{timestamp}");
1546 | 
1547 |         // Test auth config to store in keyring
1548 |         let test_auth_json =
1549 |             r#"{"type":"basic","username":"keyring-test-user","password":"keyring-test-pass"}"#;
1550 | 
1551 |         // Platform-specific keyring operations
1552 |         let (add_result, remove_result) = if cfg!(target_os = "macos") {
1553 |             // macOS using security command
1554 |             let add_result = Command::new("security")
1555 |                 .args([
1556 |                     "add-generic-password",
1557 |                     "-a",
1558 |                     &user_name,
1559 |                     "-s",
1560 |                     &service_name,
1561 |                     "-w",
1562 |                     test_auth_json,
1563 |                 ])
1564 |                 .output();
1565 | 
1566 |             let remove_result = Command::new("security")
1567 |                 .args([
1568 |                     "delete-generic-password",
1569 |                     "-a",
1570 |                     &user_name,
1571 |                     "-s",
1572 |                     &service_name,
1573 |                 ])
1574 |                 .output();
1575 | 
1576 |             (add_result, remove_result)
1577 |         } else if cfg!(target_os = "linux") {
1578 |             // Linux using secret-tool
1579 |             let add_result = Command::new("bash")
1580 |                 .args([
1581 |                     "-c",
1582 |                     &format!("echo '{test_auth_json}' | secret-tool store --label='hyper-mcp test' service '{service_name}' username '{user_name}'"),
1583 |                 ])
1584 |                 .output();
1585 | 
1586 |             let remove_result = Command::new("secret-tool")
1587 |                 .args(["clear", "service", &service_name, "username", &user_name])
1588 |                 .output();
1589 | 
1590 |             (add_result, remove_result)
1591 |         } else if cfg!(target_os = "windows") {
1592 |             // Windows using cmdkey
1593 |             let escaped_json = test_auth_json.replace("\"", "\\\"");
1594 |             let add_result = Command::new("cmdkey")
1595 |                 .args([
1596 |                     &format!("/generic:{service_name}"),
1597 |                     &format!("/user:{user_name}"),
1598 |                     &format!("/pass:{escaped_json}"),
1599 |                 ])
1600 |                 .output();
1601 | 
1602 |             let remove_result = Command::new("cmdkey")
1603 |                 .args([&format!("/delete:{service_name}")])
1604 |                 .output();
1605 | 
1606 |             (add_result, remove_result)
1607 |         } else {
1608 |             // Unsupported platform
1609 |             println!(
1610 |                 "Keyring test skipped on unsupported platform: {}",
1611 |                 std::env::consts::OS
1612 |             );
1613 |             return;
1614 |         };
1615 | 
1616 |         // Try to add the secret to keyring
1617 |         let add_output = match add_result {
1618 |             Ok(output) => output,
1619 |             Err(e) => {
1620 |                 println!("Failed to execute keyring add command: {e}. Skipping test.");
1621 |                 return;
1622 |             }
1623 |         };
1624 | 
1625 |         if !add_output.status.success() {
1626 |             println!(
1627 |                 "Failed to add secret to keyring (exit code: {}). stdout: {}, stderr: {}. Skipping test.",
1628 |                 add_output.status.code().unwrap_or(-1),
1629 |                 String::from_utf8_lossy(&add_output.stdout),
1630 |                 String::from_utf8_lossy(&add_output.stderr)
1631 |             );
1632 |             return;
1633 |         }
1634 | 
1635 |         // Test keyring auth deserialization
1636 |         let keyring_config_json =
1637 |             format!(r#"{{"type":"keyring","service":"{service_name}","user":"{user_name}"}}"#);
1638 | 
1639 |         let test_result = std::panic::catch_unwind(|| {
1640 |             let internal_auth: InternalAuthConfig =
1641 |                 serde_json::from_str(&keyring_config_json).unwrap();
1642 | 
1643 |             // This should trigger the keyring lookup and deserialize to AuthConfig
1644 |             match internal_auth {
1645 |                 InternalAuthConfig::Keyring { service, user } => {
1646 |                     assert_eq!(service, service_name);
1647 |                     assert_eq!(user, user_name);
1648 | 
1649 |                     // Test the actual keyring deserialization through AuthConfig
1650 |                     let auth_config: Result<AuthConfig, _> =
1651 |                         serde_json::from_str(&keyring_config_json);
1652 | 
1653 |                     match auth_config {
1654 |                         Ok(AuthConfig::Basic { username, password }) => {
1655 |                             assert_eq!(username, "keyring-test-user");
1656 |                             assert_eq!(password, "keyring-test-pass");
1657 |                         }
1658 |                         Ok(AuthConfig::Token { .. }) => {
1659 |                             panic!("Expected Basic auth from keyring, got Token");
1660 |                         }
1661 |                         Err(e) => {
1662 |                             println!(
1663 |                                 "Keyring lookup failed (this is expected if keyring service is not available): {e}"
1664 |                             );
1665 |                         }
1666 |                     }
1667 |                 }
1668 |                 _ => panic!("Expected Keyring internal auth config"),
1669 |             }
1670 |         });
1671 | 
1672 |         // Always attempt cleanup regardless of test result
1673 |         if let Ok(output) = remove_result {
1674 |             if !output.status.success() {
1675 |                 println!(
1676 |                     "Warning: Failed to remove test secret from keyring (exit code: {}). stdout: {}, stderr: {}",
1677 |                     output.status.code().unwrap_or(-1),
1678 |                     String::from_utf8_lossy(&output.stdout),
1679 |                     String::from_utf8_lossy(&output.stderr)
1680 |                 );
1681 |             }
1682 |         }
1683 | 
1684 |         // Re-panic if the test failed
1685 |         if let Err(panic_info) = test_result {
1686 |             std::panic::resume_unwind(panic_info);
1687 |         }
1688 |     }
1689 | 
1690 |     #[test]
1691 |     #[ignore] // Requires system keyring access and file creation - run with `cargo test -- --ignored`
1692 |     fn test_keyring_auth_complete_config_integration() {
1693 |         use std::process::Command;
1694 |         use std::time::{SystemTime, UNIX_EPOCH};
1695 |         use tokio::fs;
1696 | 
1697 |         let rt = Runtime::new().unwrap();
1698 | 
1699 |         // Generate unique identifiers
1700 |         let timestamp = SystemTime::now()
1701 |             .duration_since(UNIX_EPOCH)
1702 |             .unwrap()
1703 |             .as_secs();
1704 |         let service_name = format!("hyper-mcp-config-test-{timestamp}");
1705 |         let user_name = format!("config-test-user-{timestamp}");
1706 |         let temp_config_path = format!("test_config_{timestamp}.yaml");
1707 | 
1708 |         // Auth config to store in keyring
1709 |         let keyring_auth_json =
1710 |             r#"{"type":"token","token":"test-keyring-token-from-complete-config"}"#;
1711 | 
1712 |         // Create complete config with keyring auth
1713 |         let config_content = format!(
1714 |             r#"
1715 | auths:
1716 |   "https://keyring-test.example.com":
1717 |     type: keyring
1718 |     service: "{service_name}"
1719 |     user: "{user_name}"
1720 |   "https://basic-test.example.com":
1721 |     type: basic
1722 |     username: "basic-user"
1723 |     password: "basic-pass"
1724 | plugins:
1725 |   test_plugin:
1726 |     url: "file:///test/plugin"
1727 |     runtime_config:
1728 |       allowed_hosts:
1729 |         - "keyring-test.example.com"
1730 |         - "basic-test.example.com"
1731 | "#
1732 |         );
1733 | 
1734 |         // Platform-specific keyring operations
1735 |         let (add_result, remove_result) = if cfg!(target_os = "macos") {
1736 |             let add_result = Command::new("security")
1737 |                 .args([
1738 |                     "add-generic-password",
1739 |                     "-a",
1740 |                     &user_name,
1741 |                     "-s",
1742 |                     &service_name,
1743 |                     "-w",
1744 |                     keyring_auth_json,
1745 |                 ])
1746 |                 .output();
1747 | 
1748 |             let remove_result = Command::new("security")
1749 |                 .args([
1750 |                     "delete-generic-password",
1751 |                     "-a",
1752 |                     &user_name,
1753 |                     "-s",
1754 |                     &service_name,
1755 |                 ])
1756 |                 .output();
1757 | 
1758 |             (add_result, remove_result)
1759 |         } else if cfg!(target_os = "linux") {
1760 |             let add_result = Command::new("bash")
1761 |                 .args([
1762 |                     "-c",
1763 |                     &format!(
1764 |                         "echo '{keyring_auth_json}' | secret-tool store --label='hyper-mcp complete config test' service '{service_name}' username '{user_name}'"
1765 |                     ),
1766 |                 ])
1767 |                 .output();
1768 | 
1769 |             let remove_result = Command::new("secret-tool")
1770 |                 .args(["clear", "service", &service_name, "username", &user_name])
1771 |                 .output();
1772 | 
1773 |             (add_result, remove_result)
1774 |         } else if cfg!(target_os = "windows") {
1775 |             let escaped_json = keyring_auth_json.replace("\"", "\\\"");
1776 |             let add_result = Command::new("cmdkey")
1777 |                 .args([
1778 |                     &format!("/generic:{service_name}"),
1779 |                     &format!("/user:{user_name}"),
1780 |                     &format!("/pass:{escaped_json}"),
1781 |                 ])
1782 |                 .output();
1783 | 
1784 |             let remove_result = Command::new("cmdkey")
1785 |                 .args([&format!("/delete:{service_name}")])
1786 |                 .output();
1787 | 
1788 |             (add_result, remove_result)
1789 |         } else {
1790 |             println!(
1791 |                 "Keyring integration test skipped on unsupported platform: {}",
1792 |                 std::env::consts::OS
1793 |             );
1794 |             return;
1795 |         };
1796 | 
1797 |         // Create temporary config file
1798 |         let config_path = Path::new(&temp_config_path);
1799 |         let write_result = rt.block_on(fs::write(config_path, config_content));
1800 |         if write_result.is_err() {
1801 |             println!("Failed to create temporary config file. Skipping test.");
1802 |             return;
1803 |         }
1804 | 
1805 |         // Try to add secret to keyring
1806 |         let add_output = match add_result {
1807 |             Ok(output) => output,
1808 |             Err(e) => {
1809 |                 println!("Failed to execute keyring add command: {e}. Skipping test.");
1810 |                 let _ = rt.block_on(fs::remove_file(config_path));
1811 |                 return;
1812 |             }
1813 |         };
1814 | 
1815 |         if !add_output.status.success() {
1816 |             println!(
1817 |                 "Failed to add secret to keyring (exit code: {}). stdout: {}, stderr: {}. Skipping test.",
1818 |                 add_output.status.code().unwrap_or(-1),
1819 |                 String::from_utf8_lossy(&add_output.stdout),
1820 |                 String::from_utf8_lossy(&add_output.stderr)
1821 |             );
1822 |             let _ = rt.block_on(fs::remove_file(config_path));
1823 |             return;
1824 |         }
1825 | 
1826 |         let cli = Cli {
1827 |             config_file: Some(config_path.to_path_buf()),
1828 | 
1829 |             ..Default::default()
1830 |         };
1831 | 
1832 |         // Test loading the config file (this should trigger keyring lookup)
1833 |         let load_result = rt.block_on(load_config(&cli));
1834 | 
1835 |         // Cleanup keyring entry before checking results
1836 |         if let Ok(output) = remove_result {
1837 |             if !output.status.success() {
1838 |                 println!(
1839 |                     "Warning: Failed to remove test secret from keyring (exit code: {}). stdout: {}, stderr: {}. Manual cleanup may be required.",
1840 |                     output.status.code().unwrap_or(-1),
1841 |                     String::from_utf8_lossy(&output.stdout),
1842 |                     String::from_utf8_lossy(&output.stderr)
1843 |                 );
1844 |             }
1845 |         }
1846 | 
1847 |         // Cleanup temporary config file
1848 |         let _ = rt.block_on(fs::remove_file(config_path));
1849 | 
1850 |         // Now check the test results
1851 |         match load_result {
1852 |             Ok(config) => {
1853 |                 // Verify auths are present
1854 |                 assert!(
1855 |                     config.auths.is_some(),
1856 |                     "Expected auths to be present in loaded config"
1857 |                 );
1858 |                 let auths = config.auths.unwrap();
1859 |                 assert_eq!(auths.len(), 2, "Expected 2 auth configurations");
1860 | 
1861 |                 // Verify keyring auth was resolved successfully
1862 |                 let keyring_url = Url::parse("https://keyring-test.example.com").unwrap();
1863 |                 assert!(
1864 |                     auths.contains_key(&keyring_url),
1865 |                     "Expected keyring auth URL to be present"
1866 |                 );
1867 | 
1868 |                 match &auths[&keyring_url] {
1869 |                     AuthConfig::Token { token } => {
1870 |                         assert_eq!(
1871 |                             token, "test-keyring-token-from-complete-config",
1872 |                             "Token from keyring should match stored value"
1873 |                         );
1874 |                     }
1875 |                     _ => panic!("Expected Token auth from keyring resolution"),
1876 |                 }
1877 | 
1878 |                 // Verify basic auth still works alongside keyring auth
1879 |                 let basic_url = Url::parse("https://basic-test.example.com").unwrap();
1880 |                 assert!(
1881 |                     auths.contains_key(&basic_url),
1882 |                     "Expected basic auth URL to be present"
1883 |                 );
1884 | 
1885 |                 match &auths[&basic_url] {
1886 |                     AuthConfig::Basic { username, password } => {
1887 |                         assert_eq!(username, "basic-user");
1888 |                         assert_eq!(password, "basic-pass");
1889 |                     }
1890 |                     _ => panic!("Expected Basic auth config"),
1891 |                 }
1892 | 
1893 |                 // Verify plugins loaded correctly
1894 |                 assert_eq!(config.plugins.len(), 1, "Expected 1 plugin in config");
1895 |                 assert!(
1896 |                     config
1897 |                         .plugins
1898 |                         .contains_key(&PluginName("test_plugin".to_string()))
1899 |                 );
1900 | 
1901 |                 println!(
1902 |                     "✅ Keyring integration test passed on platform: {}",
1903 |                     std::env::consts::OS
1904 |                 );
1905 |             }
1906 |             Err(e) => {
1907 |                 // Check if this is a keyring-related error
1908 |                 let error_msg = e.to_string();
1909 |                 if error_msg.contains("keyring") || error_msg.contains("secure storage") {
1910 |                     println!(
1911 |                         "Keyring lookup failed (keyring service may not be available): {e}. This is acceptable for CI environments."
1912 |                     );
1913 |                 } else {
1914 |                     panic!("Unexpected error loading config with keyring auth: {e}");
1915 |                 }
1916 |             }
1917 |         }
1918 |     }
1919 | 
1920 |     #[test]
1921 |     #[ignore] // Requires system keyring access - run with `cargo test -- --ignored`
1922 |     fn test_keyring_auth_direct_deserialization() {
1923 |         use std::process::Command;
1924 |         use std::time::{SystemTime, UNIX_EPOCH};
1925 | 
1926 |         // Generate unique service and user names to avoid conflicts
1927 |         let timestamp = SystemTime::now()
1928 |             .duration_since(UNIX_EPOCH)
1929 |             .unwrap()
1930 |             .as_secs();
1931 |         let service_name = format!("hyper-mcp-direct-test-{timestamp}");
1932 |         let user_name = format!("direct-test-user-{timestamp}");
1933 | 
1934 |         // Test auth config to store in keyring (basic auth this time)
1935 |         let test_auth_json =
1936 |             r#"{"type":"basic","username":"direct-keyring-user","password":"direct-keyring-pass"}"#;
1937 | 
1938 |         // Determine platform and execute appropriate keyring commands
1939 |         if cfg!(target_os = "macos") {
1940 |             // macOS: Add and test, then cleanup
1941 |             let add_cmd = Command::new("security")
1942 |                 .args([
1943 |                     "add-generic-password",
1944 |                     "-a",
1945 |                     &user_name,
1946 |                     "-s",
1947 |                     &service_name,
1948 |                     "-w",
1949 |                     test_auth_json,
1950 |                 ])
1951 |                 .output();
1952 | 
1953 |             if let Ok(add_output) = add_cmd {
1954 |                 if add_output.status.success() {
1955 |                     // Test the keyring deserialization
1956 |                     let keyring_config_json = format!(
1957 |                         r#"{{"type":"keyring","service":"{service_name}","user":"{user_name}"}}"#
1958 |                     );
1959 | 
1960 |                     let auth_result: Result<AuthConfig, _> =
1961 |                         serde_json::from_str(&keyring_config_json);
1962 | 
1963 |                     // Cleanup first
1964 |                     let _ = Command::new("security")
1965 |                         .args([
1966 |                             "delete-generic-password",
1967 |                             "-a",
1968 |                             &user_name,
1969 |                             "-s",
1970 |                             &service_name,
1971 |                         ])
1972 |                         .output();
1973 | 
1974 |                     // Verify result
1975 |                     match auth_result {
1976 |                         Ok(AuthConfig::Basic { username, password }) => {
1977 |                             assert_eq!(username, "direct-keyring-user");
1978 |                             assert_eq!(password, "direct-keyring-pass");
1979 |                             println!("✅ macOS keyring direct deserialization test passed");
1980 |                         }
1981 |                         Ok(_) => panic!("Expected Basic auth from keyring"),
1982 |                         Err(e) => {
1983 |                             println!(
1984 |                                 "Keyring lookup failed on macOS (may not be available in CI): {e}"
1985 |                             );
1986 |                         }
1987 |                     }
1988 |                 } else {
1989 |                     println!("Failed to add secret to macOS keyring, skipping test");
1990 |                 }
1991 |             }
1992 |         } else if cfg!(target_os = "linux") {
1993 |             // Linux: Add and test, then cleanup
1994 |             let add_cmd = Command::new("bash")
1995 |                 .args([
1996 |                     "-c",
1997 |                     &format!(
1998 |                         "echo '{test_auth_json}' | secret-tool store --label='hyper-mcp direct test' service '{service_name}' username '{user_name}'"
1999 |                     ),
2000 |                 ])
2001 |                 .output();
2002 | 
2003 |             if let Ok(add_output) = add_cmd {
2004 |                 if add_output.status.success() {
2005 |                     // Test the keyring deserialization
2006 |                     let keyring_config_json = format!(
2007 |                         r#"{{"type":"keyring","service":"{service_name}","user":"{user_name}"}}"#
2008 |                     );
2009 | 
2010 |                     let auth_result: Result<AuthConfig, _> =
2011 |                         serde_json::from_str(&keyring_config_json);
2012 | 
2013 |                     // Cleanup first
2014 |                     let _ = Command::new("secret-tool")
2015 |                         .args(["clear", "service", &service_name, "username", &user_name])
2016 |                         .output();
2017 | 
2018 |                     // Verify result
2019 |                     match auth_result {
2020 |                         Ok(AuthConfig::Basic { username, password }) => {
2021 |                             assert_eq!(username, "direct-keyring-user");
2022 |                             assert_eq!(password, "direct-keyring-pass");
2023 |                             println!("✅ Linux keyring direct deserialization test passed");
2024 |                         }
2025 |                         Ok(_) => panic!("Expected Basic auth from keyring"),
2026 |                         Err(e) => {
2027 |                             println!(
2028 |                                 "Keyring lookup failed on Linux (may not be available in CI): {e}"
2029 |                             );
2030 |                         }
2031 |                     }
2032 |                 } else {
2033 |                     println!("Failed to add secret to Linux keyring, skipping test");
2034 |                 }
2035 |             }
2036 |         } else if cfg!(target_os = "windows") {
2037 |             // Windows: Add and test, then cleanup
2038 |             let escaped_json = test_auth_json.replace("\"", "\\\"");
2039 |             let add_cmd = Command::new("cmdkey")
2040 |                 .args([
2041 |                     &format!("/generic:{service_name}"),
2042 |                     &format!("/user:{user_name}"),
2043 |                     &format!("/pass:{escaped_json}"),
2044 |                 ])
2045 |                 .output();
2046 | 
2047 |             if let Ok(add_output) = add_cmd {
2048 |                 if add_output.status.success() {
2049 |                     // Test the keyring deserialization
2050 |                     let keyring_config_json = format!(
2051 |                         r#"{{"type":"keyring","service":"{service_name}","user":"{user_name}"}}"#
2052 |                     );
2053 | 
2054 |                     let auth_result: Result<AuthConfig, _> =
2055 |                         serde_json::from_str(&keyring_config_json);
2056 | 
2057 |                     // Cleanup first
2058 |                     let _ = Command::new("cmdkey")
2059 |                         .args([&format!("/delete:{service_name}")])
2060 |                         .output();
2061 | 
2062 |                     // Verify result
2063 |                     match auth_result {
2064 |                         Ok(AuthConfig::Basic { username, password }) => {
2065 |                             assert_eq!(username, "direct-keyring-user");
2066 |                             assert_eq!(password, "direct-keyring-pass");
2067 |                             println!("✅ Windows keyring direct deserialization test passed");
2068 |                         }
2069 |                         Ok(_) => panic!("Expected Basic auth from keyring"),
2070 |                         Err(e) => {
2071 |                             println!(
2072 |                                 "Keyring lookup failed on Windows (may not be available in CI): {e}"
2073 |                             );
2074 |                         }
2075 |                     }
2076 |                 } else {
2077 |                     println!("Failed to add secret to Windows keyring, skipping test");
2078 |                 }
2079 |             }
2080 |         } else {
2081 |             println!(
2082 |                 "Direct keyring deserialization test skipped on unsupported platform: {}",
2083 |                 std::env::consts::OS
2084 |             );
2085 |         }
2086 |     }
2087 | 
2088 |     #[test]
2089 |     fn test_platform_detection_and_keyring_tool_availability() {
2090 |         use std::process::Command;
2091 | 
2092 |         println!(
2093 |             "Running platform detection test on: {}",
2094 |             std::env::consts::OS
2095 |         );
2096 | 
2097 |         if cfg!(target_os = "macos") {
2098 |             // Test macOS security command availability
2099 |             let security_check = Command::new("security").arg("help").output();
2100 | 
2101 |             match security_check {
2102 |                 Ok(output) => {
2103 |                     if output.status.success() {
2104 |                         println!("✅ macOS security command is available");
2105 | 
2106 |                         // Test that we can list keychains (read-only operation)
2107 |                         let list_check = Command::new("security").args(["list-keychains"]).output();
2108 |                         match list_check {
2109 |                             Ok(list_output) if list_output.status.success() => {
2110 |                                 println!("✅ macOS keychain access is functional");
2111 |                             }
2112 |                             _ => {
2113 |                                 println!("⚠️  macOS keychain access may be limited");
2114 |                             }
2115 |                         }
2116 |                     } else {
2117 |                         println!("❌ macOS security command failed");
2118 |                     }
2119 |                 }
2120 |                 Err(e) => {
2121 |                     println!("❌ macOS security command not found: {e}");
2122 |                 }
2123 |             }
2124 |         } else if cfg!(target_os = "linux") {
2125 |             // Test Linux secret-tool availability
2126 |             let secret_tool_check = Command::new("secret-tool").arg("--help").output();
2127 | 
2128 |             match secret_tool_check {
2129 |                 Ok(output) => {
2130 |                     if output.status.success() {
2131 |                         println!("✅ Linux secret-tool is available");
2132 |                     } else {
2133 |                         println!("❌ Linux secret-tool command failed");
2134 |                     }
2135 |                 }
2136 |                 Err(e) => {
2137 |                     println!(
2138 |                         "❌ Linux secret-tool not found: {e}. Install with: sudo apt-get install libsecret-tools"
2139 |                     );
2140 |                 }
2141 |             }
2142 | 
2143 |             // Check if dbus session is available (required for keyring)
2144 |             let dbus_check = Command::new("dbus-send")
2145 |                 .args([
2146 |                     "--session",
2147 |                     "--dest=org.freedesktop.DBus",
2148 |                     "--print-reply",
2149 |                     "/org/freedesktop/DBus",
2150 |                     "org.freedesktop.DBus.ListNames",
2151 |                 ])
2152 |                 .output();
2153 | 
2154 |             match dbus_check {
2155 |                 Ok(output) if output.status.success() => {
2156 |                     println!("✅ Linux D-Bus session is available");
2157 |                 }
2158 |                 _ => {
2159 |                     println!("⚠️  Linux D-Bus session may not be available (required for keyring)");
2160 |                 }
2161 |             }
2162 |         } else if cfg!(target_os = "windows") {
2163 |             // Test Windows cmdkey availability
2164 |             let cmdkey_check = Command::new("cmdkey").arg("/?").output();
2165 | 
2166 |             match cmdkey_check {
2167 |                 Ok(output) => {
2168 |                     if output.status.success() {
2169 |                         println!("✅ Windows cmdkey is available");
2170 | 
2171 |                         // Test that we can list credentials (read-only operation)
2172 |                         let list_check = Command::new("cmdkey").args(["/list"]).output();
2173 |                         match list_check {
2174 |                             Ok(list_output) if list_output.status.success() => {
2175 |                                 println!("✅ Windows Credential Manager access is functional");
2176 |                             }
2177 |                             _ => {
2178 |                                 println!("⚠️  Windows Credential Manager access may be limited");
2179 |                             }
2180 |                         }
2181 |                     } else {
2182 |                         println!("❌ Windows cmdkey command failed");
2183 |                     }
2184 |                 }
2185 |                 Err(e) => {
2186 |                     println!("❌ Windows cmdkey not found: {e}");
2187 |                 }
2188 |             }
2189 |         } else {
2190 |             println!(
2191 |                 "ℹ️  Platform {} is not supported for keyring authentication",
2192 |                 std::env::consts::OS
2193 |             );
2194 |         }
2195 |     }
2196 | 
2197 |     #[test]
2198 |     fn test_keyring_auth_config_missing_service() {
2199 |         let json = r#"{"type":"keyring","user":"test-user"}"#;
2200 |         let result: Result<InternalAuthConfig, _> = serde_json::from_str(json);
2201 |         assert!(result.is_err(), "Expected error for missing service field");
2202 |     }
2203 | 
2204 |     #[test]
2205 |     fn test_keyring_auth_config_missing_user() {
2206 |         let json = r#"{"type":"keyring","service":"test-service"}"#;
2207 |         let result: Result<InternalAuthConfig, _> = serde_json::from_str(json);
2208 |         assert!(result.is_err(), "Expected error for missing user field");
2209 |     }
2210 | 
2211 |     #[test]
2212 |     fn test_keyring_auth_config_empty_values() {
2213 |         let json = r#"{"type":"keyring","service":"","user":"test-user"}"#;
2214 |         let internal_auth: InternalAuthConfig = serde_json::from_str(json).unwrap();
2215 | 
2216 |         match internal_auth {
2217 |             InternalAuthConfig::Keyring { service, user } => {
2218 |                 assert_eq!(service, "");
2219 |                 assert_eq!(user, "test-user");
2220 |             }
2221 |             _ => panic!("Expected Keyring auth config"),
2222 |         }
2223 |     }
2224 | 
2225 |     #[test]
2226 |     fn test_mixed_auth_types_config() {
2227 |         let json = r#"
2228 | {
2229 |   "auths": {
2230 |     "https://basic.example.com": {
2231 |       "type": "basic",
2232 |       "username": "basicuser",
2233 |       "password": "basicpass"
2234 |     },
2235 |     "https://token.example.com": {
2236 |       "type": "token",
2237 |       "token": "token-123"
2238 |     }
2239 |   },
2240 |   "plugins": {
2241 |     "test_plugin": {
2242 |       "url": "file:///path/to/plugin"
2243 |     }
2244 |   }
2245 | }
2246 | "#;
2247 | 
2248 |         let config: Config = serde_json::from_str(json).unwrap();
2249 |         assert!(config.auths.is_some());
2250 | 
2251 |         let auths = config.auths.unwrap();
2252 |         assert_eq!(auths.len(), 2);
2253 | 
2254 |         // Verify we have both auth types
2255 |         let basic_url = Url::parse("https://basic.example.com").unwrap();
2256 |         let token_url = Url::parse("https://token.example.com").unwrap();
2257 | 
2258 |         match &auths[&basic_url] {
2259 |             AuthConfig::Basic { username, password } => {
2260 |                 assert_eq!(username, "basicuser");
2261 |                 assert_eq!(password, "basicpass");
2262 |             }
2263 |             _ => panic!("Expected Basic auth"),
2264 |         }
2265 | 
2266 |         match &auths[&token_url] {
2267 |             AuthConfig::Token { token } => {
2268 |                 assert_eq!(token, "token-123");
2269 |             }
2270 |             _ => panic!("Expected Token auth"),
2271 |         }
2272 |     }
2273 | 
2274 |     #[test]
2275 |     fn test_auth_config_yaml_mixed_types() {
2276 |         let yaml = r#"
2277 | auths:
2278 |   "https://basic.example.com":
2279 |     type: basic
2280 |     username: basicuser
2281 |     password: basicpass
2282 |   "https://token.example.com":
2283 |     type: token
2284 |     token: token-123
2285 | plugins:
2286 |   test_plugin:
2287 |     url: "file:///path/to/plugin"
2288 | "#;
2289 | 
2290 |         let config: Config = serde_yaml::from_str(yaml).unwrap();
2291 |         assert!(config.auths.is_some());
2292 | 
2293 |         let auths = config.auths.unwrap();
2294 |         assert_eq!(auths.len(), 2);
2295 |     }
2296 | 
2297 |     #[test]
2298 |     fn test_auth_config_special_urls() {
2299 |         let mut auths = HashMap::new();
2300 | 
2301 |         // Test with localhost URL
2302 |         let localhost_url = Url::parse("http://localhost:8080").unwrap();
2303 |         auths.insert(
2304 |             localhost_url.clone(),
2305 |             AuthConfig::Basic {
2306 |                 username: "localuser".to_string(),
2307 |                 password: "localpass".to_string(),
2308 |             },
2309 |         );
2310 | 
2311 |         // Test with IP address URL
2312 |         let ip_url = Url::parse("https://192.168.1.100:443").unwrap();
2313 |         auths.insert(
2314 |             ip_url.clone(),
2315 |             AuthConfig::Token {
2316 |                 token: "ip-token".to_string(),
2317 |             },
2318 |         );
2319 | 
2320 |         // Test with custom port
2321 |         let custom_port_url = Url::parse("https://api.example.com:9000").unwrap();
2322 |         auths.insert(
2323 |             custom_port_url.clone(),
2324 |             AuthConfig::Basic {
2325 |                 username: "portuser".to_string(),
2326 |                 password: "portpass".to_string(),
2327 |             },
2328 |         );
2329 | 
2330 |         let config = Config {
2331 |             auths: Some(auths),
2332 |             plugins: HashMap::new(),
2333 | 
2334 |             ..Default::default()
2335 |         };
2336 | 
2337 |         // Test serialization and deserialization round-trip
2338 |         let json = serde_json::to_string(&config).unwrap();
2339 |         let deserialized: Config = serde_json::from_str(&json).unwrap();
2340 | 
2341 |         assert!(deserialized.auths.is_some());
2342 |         let deserialized_auths = deserialized.auths.unwrap();
2343 |         assert_eq!(deserialized_auths.len(), 3);
2344 | 
2345 |         assert!(deserialized_auths.contains_key(&localhost_url));
2346 |         assert!(deserialized_auths.contains_key(&ip_url));
2347 |         assert!(deserialized_auths.contains_key(&custom_port_url));
2348 |     }
2349 | 
2350 |     #[test]
2351 |     fn test_auth_config_unicode_values() {
2352 |         // Test with unicode characters in credentials
2353 |         let auth_config = AuthConfig::Basic {
2354 |             username: "用户名".to_string(),
2355 |             password: "密码🔐".to_string(),
2356 |         };
2357 | 
2358 |         let json = serde_json::to_string(&auth_config).unwrap();
2359 |         let deserialized: AuthConfig = serde_json::from_str(&json).unwrap();
2360 | 
2361 |         match deserialized {
2362 |             AuthConfig::Basic { username, password } => {
2363 |                 assert_eq!(username, "用户名");
2364 |                 assert_eq!(password, "密码🔐");
2365 |             }
2366 |             _ => panic!("Expected Basic auth config"),
2367 |         }
2368 |     }
2369 | 
2370 |     #[test]
2371 |     fn test_auth_config_long_token() {
2372 |         // Test with very long token (JWT-like)
2373 |         let long_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE2NzAyODYyNjMifQ.eyJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImV4cCI6MTYzNzI4NjI2MywiaWF0IjoxNjM3Mjc5MDYzLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyQGV4YW1wbGUuY29tIn0.signature_here_would_be_much_longer";
2374 | 
2375 |         let auth_config = AuthConfig::Token {
2376 |             token: long_token.to_string(),
2377 |         };
2378 | 
2379 |         let json = serde_json::to_string(&auth_config).unwrap();
2380 |         let deserialized: AuthConfig = serde_json::from_str(&json).unwrap();
2381 | 
2382 |         match deserialized {
2383 |             AuthConfig::Token { token } => {
2384 |                 assert_eq!(token, long_token);
2385 |                 assert!(token.len() > 200);
2386 |             }
2387 |             _ => panic!("Expected Token auth config"),
2388 |         }
2389 |     }
2390 | 
2391 |     // Tests for skip_tools Option<RegexSet> functionality
2392 |     #[test]
2393 |     fn test_skip_tools_none() {
2394 |         let runtime_config = RuntimeConfig {
2395 |             skip_prompts: None,
2396 |             skip_resource_templates: None,
2397 |             skip_resources: None,
2398 |             skip_tools: None,
2399 |             allowed_hosts: None,
2400 |             allowed_paths: None,
2401 |             env_vars: None,
2402 |             memory_limit: None,
2403 |         };
2404 | 
2405 |         // Test serialization
2406 |         let json = serde_json::to_string(&runtime_config).unwrap();
2407 |         assert!(json.contains("\"skip_tools\":null"));
2408 | 
2409 |         // Test deserialization
2410 |         let deserialized: RuntimeConfig = serde_json::from_str(&json).unwrap();
2411 |         assert!(deserialized.skip_tools.is_none());
2412 |     }
2413 | 
2414 |     #[test]
2415 |     fn test_skip_tools_some_basic() {
2416 |         let json = r#"{
2417 |             "skip_tools": ["tool1", "tool2", "tool3"]
2418 |         }"#;
2419 | 
2420 |         let runtime_config: RuntimeConfig = serde_json::from_str(json).unwrap();
2421 |         let skip_tools = runtime_config.skip_tools.as_ref().unwrap();
2422 | 
2423 |         assert_eq!(skip_tools.len(), 3);
2424 |         assert!(skip_tools.is_match("tool1"));
2425 |         assert!(skip_tools.is_match("tool2"));
2426 |         assert!(skip_tools.is_match("tool3"));
2427 |         assert!(!skip_tools.is_match("tool4"));
2428 |         assert!(!skip_tools.is_match("tool1_extended"));
2429 |     }
2430 | 
2431 |     #[test]
2432 |     fn test_skip_tools_regex_patterns() {
2433 |         let json = r#"{
2434 |             "skip_tools": ["tool.*", "debug_.*", "test_[0-9]+"]
2435 |         }"#;
2436 | 
2437 |         let runtime_config: RuntimeConfig = serde_json::from_str(json).unwrap();
2438 |         let skip_tools = runtime_config.skip_tools.as_ref().unwrap();
2439 | 
2440 |         // Test wildcard patterns
2441 |         assert!(skip_tools.is_match("tool1"));
2442 |         assert!(skip_tools.is_match("tool_anything"));
2443 |         assert!(skip_tools.is_match("toolbox"));
2444 | 
2445 |         // Test prefix patterns
2446 |         assert!(skip_tools.is_match("debug_info"));
2447 |         assert!(skip_tools.is_match("debug_error"));
2448 | 
2449 |         // Test numbered patterns
2450 |         assert!(skip_tools.is_match("test_1"));
2451 |         assert!(skip_tools.is_match("test_99"));
2452 | 
2453 |         // Test non-matches
2454 |         assert!(!skip_tools.is_match("my_tool"));
2455 |         assert!(!skip_tools.is_match("debug"));
2456 |         assert!(!skip_tools.is_match("test_abc"));
2457 |         // "tool" should match "tool.*" pattern since it becomes "^tool.*$"
2458 |         assert!(skip_tools.is_match("tool"));
2459 |     }
2460 | 
2461 |     #[test]
2462 |     fn test_skip_tools_anchoring_behavior() {
2463 |         let json = r#"{
2464 |             "skip_tools": ["tool", "^prefix_.*", ".*_suffix$", "^exact_match$"]
2465 |         }"#;
2466 | 
2467 |         let runtime_config: RuntimeConfig = serde_json::from_str(json).unwrap();
2468 |         let skip_tools = runtime_config.skip_tools.as_ref().unwrap();
2469 | 
2470 |         // "tool" should be auto-anchored to "^tool$"
2471 |         assert!(skip_tools.is_match("tool"));
2472 |         assert!(!skip_tools.is_match("tool_extended"));
2473 |         assert!(!skip_tools.is_match("my_tool"));
2474 | 
2475 |         // "^prefix_.*" should match anything starting with "prefix_"
2476 |         assert!(skip_tools.is_match("prefix_anything"));
2477 |         assert!(skip_tools.is_match("prefix_"));
2478 |         assert!(!skip_tools.is_match("my_prefix_tool"));
2479 | 
2480 |         // ".*_suffix$" should match anything ending with "_suffix"
2481 |         assert!(skip_tools.is_match("any_suffix"));
2482 |         assert!(skip_tools.is_match("_suffix"));
2483 |         assert!(!skip_tools.is_match("suffix_extended"));
2484 | 
2485 |         // "^exact_match$" should only match exactly "exact_match"
2486 |         assert!(skip_tools.is_match("exact_match"));
2487 |         assert!(!skip_tools.is_match("exact_match_extended"));
2488 |         // "prefix_exact_match" matches "^prefix_.*" pattern, not "^exact_match$"
2489 |         assert!(skip_tools.is_match("prefix_exact_match"));
2490 |     }
2491 | 
2492 |     #[test]
2493 |     fn test_skip_tools_serialization_roundtrip() {
2494 |         let original_patterns = vec![
2495 |             "tool1".to_string(),
2496 |             "tool.*".to_string(),
2497 |             "debug_.*".to_string(),
2498 |         ];
2499 |         let regex_set = RegexSet::new(&original_patterns).unwrap();
2500 | 
2501 |         let runtime_config = RuntimeConfig {
2502 |             skip_prompts: None,
2503 |             skip_resource_templates: None,
2504 |             skip_resources: None,
2505 |             skip_tools: Some(regex_set),
2506 |             allowed_hosts: None,
2507 |             allowed_paths: None,
2508 |             env_vars: None,
2509 |             memory_limit: None,
2510 |         };
2511 | 
2512 |         // Serialize
2513 |         let json = serde_json::to_string(&runtime_config).unwrap();
2514 | 
2515 |         // Deserialize
2516 |         let deserialized: RuntimeConfig = serde_json::from_str(&json).unwrap();
2517 |         let skip_tools = deserialized.skip_tools.as_ref().unwrap();
2518 | 
2519 |         // Verify functionality is preserved
2520 |         assert!(skip_tools.is_match("tool1"));
2521 |         assert!(skip_tools.is_match("tool_anything"));
2522 |         assert!(skip_tools.is_match("debug_info"));
2523 |         assert!(!skip_tools.is_match("other_tool"));
2524 |     }
2525 | 
2526 |     #[test]
2527 |     fn test_skip_tools_yaml_deserialization() {
2528 |         let yaml = r#"
2529 | skip_tools:
2530 |   - "tool1"
2531 |   - "tool.*"
2532 |   - "debug_.*"
2533 | allowed_hosts:
2534 |   - "example.com"
2535 | "#;
2536 | 
2537 |         let runtime_config: RuntimeConfig = serde_yaml::from_str(yaml).unwrap();
2538 |         let skip_tools = runtime_config.skip_tools.as_ref().unwrap();
2539 | 
2540 |         assert!(skip_tools.is_match("tool1"));
2541 |         assert!(skip_tools.is_match("tool_test"));
2542 |         assert!(skip_tools.is_match("debug_info"));
2543 |         assert!(!skip_tools.is_match("other"));
2544 |     }
2545 | 
2546 |     #[test]
2547 |     fn test_skip_tools_invalid_regex() {
2548 |         let json = r#"{
2549 |             "skip_tools": ["valid_tool", "[unclosed_bracket", "another_valid"]
2550 |         }"#;
2551 | 
2552 |         let result: Result<RuntimeConfig, _> = serde_json::from_str(json);
2553 |         assert!(result.is_err());
2554 | 
2555 |         let error_msg = result.unwrap_err().to_string();
2556 |         assert!(error_msg.contains("regex") || error_msg.contains("bracket"));
2557 |     }
2558 | 
2559 |     #[test]
2560 |     fn test_skip_tools_empty_patterns() {
2561 |         let json = r#"{
2562 |             "skip_tools": []
2563 |         }"#;
2564 | 
2565 |         let runtime_config: RuntimeConfig = serde_json::from_str(json).unwrap();
2566 |         let skip_tools = runtime_config.skip_tools.as_ref().unwrap();
2567 | 
2568 |         assert_eq!(skip_tools.len(), 0);
2569 |         assert!(!skip_tools.is_match("anything"));
2570 |     }
2571 | 
2572 |     #[test]
2573 |     fn test_skip_tools_special_regex_characters() {
2574 |         let json = r#"{
2575 |             "skip_tools": ["tool\\.exe", "script\\?", "temp\\*file"]
2576 |         }"#;
2577 | 
2578 |         let runtime_config: RuntimeConfig = serde_json::from_str(json).unwrap();
2579 |         let skip_tools = runtime_config.skip_tools.as_ref().unwrap();
2580 | 
2581 |         // Test literal matching of special characters
2582 |         assert!(skip_tools.is_match("tool.exe"));
2583 |         assert!(skip_tools.is_match("script?"));
2584 |         assert!(skip_tools.is_match("temp*file"));
2585 | 
2586 |         // These should not match due to anchoring
2587 |         assert!(!skip_tools.is_match("my_tool.exe"));
2588 |         assert!(!skip_tools.is_match("script?.bat"));
2589 |     }
2590 | 
2591 |     #[test]
2592 |     fn test_skip_tools_case_sensitivity() {
2593 |         let json = r#"{
2594 |             "skip_tools": ["Tool", "DEBUG.*"]
2595 |         }"#;
2596 | 
2597 |         let runtime_config: RuntimeConfig = serde_json::from_str(json).unwrap();
2598 |         let skip_tools = runtime_config.skip_tools.as_ref().unwrap();
2599 | 
2600 |         // RegexSet is case sensitive by default
2601 |         assert!(skip_tools.is_match("Tool"));
2602 |         assert!(!skip_tools.is_match("tool"));
2603 |         assert!(!skip_tools.is_match("TOOL"));
2604 | 
2605 |         assert!(skip_tools.is_match("DEBUG_info"));
2606 |         assert!(!skip_tools.is_match("debug_info"));
2607 |     }
2608 | 
2609 |     #[test]
2610 |     fn test_skip_tools_default_behavior() {
2611 |         // Test that skip_tools defaults to None when not specified
2612 |         let json = r#"{
2613 |             "allowed_hosts": ["example.com"]
2614 |         }"#;
2615 | 
2616 |         let runtime_config: RuntimeConfig = serde_json::from_str(json).unwrap();
2617 |         assert!(runtime_config.skip_tools.is_none());
2618 |     }
2619 | 
2620 |     #[test]
2621 |     fn test_skip_tools_matching_functionality() {
2622 |         let patterns = vec![
2623 |             "exact".to_string(),
2624 |             "prefix.*".to_string(),
2625 |             ".*suffix".to_string(),
2626 |         ];
2627 |         let regex_set = RegexSet::new(
2628 |             patterns
2629 |                 .iter()
2630 |                 .map(|p| format!("^{}$", p))
2631 |                 .collect::<Vec<_>>(),
2632 |         )
2633 |         .unwrap();
2634 | 
2635 |         // Test exact match
2636 |         assert!(regex_set.is_match("exact"));
2637 |         assert!(!regex_set.is_match("exact_more"));
2638 | 
2639 |         // Test prefix match
2640 |         assert!(regex_set.is_match("prefix123"));
2641 |         assert!(regex_set.is_match("prefixABC"));
2642 |         assert!(!regex_set.is_match("not_prefix123"));
2643 | 
2644 |         // Test suffix match
2645 |         assert!(regex_set.is_match("anysuffix"));
2646 |         assert!(regex_set.is_match("123suffix"));
2647 |         assert!(!regex_set.is_match("suffix_more"));
2648 |     }
2649 | 
2650 |     #[test]
2651 |     fn test_skip_tools_examples_integration() {
2652 |         let rt = Runtime::new().unwrap();
2653 | 
2654 |         // Load the skip_tools examples config
2655 |         let path = Path::new("tests/fixtures/skip_tools_examples.yaml");
2656 |         let cli = Cli {
2657 |             config_file: Some(path.to_path_buf()),
2658 | 
2659 |             ..Default::default()
2660 |         };
2661 | 
2662 |         let config_result = rt.block_on(load_config(&cli));
2663 |         assert!(
2664 |             config_result.is_ok(),
2665 |             "Failed to load skip_tools examples config"
2666 |         );
2667 | 
2668 |         let config = config_result.unwrap();
2669 |         assert_eq!(
2670 |             config.plugins.len(),
2671 |             10,
2672 |             "Expected 10 plugins in the config"
2673 |         );
2674 | 
2675 |         // Test exact_match_plugin
2676 |         let exact_plugin = &config.plugins[&PluginName("exact_match_plugin".to_string())];
2677 |         let exact_skip_tools = exact_plugin
2678 |             .runtime_config
2679 |             .as_ref()
2680 |             .unwrap()
2681 |             .skip_tools
2682 |             .as_ref()
2683 |             .unwrap();
2684 |         assert!(exact_skip_tools.is_match("debug_tool"));
2685 |         assert!(exact_skip_tools.is_match("test_runner"));
2686 |         assert!(exact_skip_tools.is_match("deprecated_helper"));
2687 |         assert!(!exact_skip_tools.is_match("other_tool"));
2688 |         assert!(!exact_skip_tools.is_match("debug_tool_extended"));
2689 | 
2690 |         // Test wildcard_plugin
2691 |         let wildcard_plugin = &config.plugins[&PluginName("wildcard_plugin".to_string())];
2692 |         let wildcard_skip_tools = wildcard_plugin
2693 |             .runtime_config
2694 |             .as_ref()
2695 |             .unwrap()
2696 |             .skip_tools
2697 |             .as_ref()
2698 |             .unwrap();
2699 |         assert!(wildcard_skip_tools.is_match("temp_file"));
2700 |         assert!(wildcard_skip_tools.is_match("temp_data"));
2701 |         assert!(wildcard_skip_tools.is_match("file_backup"));
2702 |         assert!(wildcard_skip_tools.is_match("data_backup"));
2703 |         assert!(wildcard_skip_tools.is_match("debug"));
2704 |         assert!(wildcard_skip_tools.is_match("debugger"));
2705 |         assert!(!wildcard_skip_tools.is_match("backup_file"));
2706 |         assert!(!wildcard_skip_tools.is_match("temp"));
2707 | 
2708 |         // Test regex_plugin
2709 |         let regex_plugin = &config.plugins[&PluginName("regex_plugin".to_string())];
2710 |         let regex_skip_tools = regex_plugin
2711 |             .runtime_config
2712 |             .as_ref()
2713 |             .unwrap()
2714 |             .skip_tools
2715 |             .as_ref()
2716 |             .unwrap();
2717 |         assert!(regex_skip_tools.is_match("tool_1"));
2718 |         assert!(regex_skip_tools.is_match("tool_42"));
2719 |         assert!(regex_skip_tools.is_match("test_unit"));
2720 |         assert!(regex_skip_tools.is_match("test_integration"));
2721 |         assert!(regex_skip_tools.is_match("data_helper"));
2722 |         assert!(!regex_skip_tools.is_match("tool_abc"));
2723 |         assert!(!regex_skip_tools.is_match("test_system"));
2724 |         assert!(!regex_skip_tools.is_match("Data_helper"));
2725 | 
2726 |         // Test anchored_plugin
2727 |         let anchored_plugin = &config.plugins[&PluginName("anchored_plugin".to_string())];
2728 |         let anchored_skip_tools = anchored_plugin
2729 |             .runtime_config
2730 |             .as_ref()
2731 |             .unwrap()
2732 |             .skip_tools
2733 |             .as_ref()
2734 |             .unwrap();
2735 |         assert!(anchored_skip_tools.is_match("system_tool"));
2736 |         assert!(anchored_skip_tools.is_match("data_internal"));
2737 |         assert!(anchored_skip_tools.is_match("exact_only"));
2738 |         assert!(!anchored_skip_tools.is_match("my_system_tool"));
2739 |         assert!(!anchored_skip_tools.is_match("data_internal_ext"));
2740 |         assert!(!anchored_skip_tools.is_match("exact_only_more"));
2741 | 
2742 |         // Test case_sensitive_plugin
2743 |         let case_plugin = &config.plugins[&PluginName("case_sensitive_plugin".to_string())];
2744 |         let case_skip_tools = case_plugin
2745 |             .runtime_config
2746 |             .as_ref()
2747 |             .unwrap()
2748 |             .skip_tools
2749 |             .as_ref()
2750 |             .unwrap();
2751 |         assert!(case_skip_tools.is_match("Tool"));
2752 |         assert!(!case_skip_tools.is_match("tool"));
2753 |         assert!(!case_skip_tools.is_match("TOOL"));
2754 |         assert!(case_skip_tools.is_match("DEBUG_info"));
2755 |         assert!(!case_skip_tools.is_match("debug_info"));
2756 |         assert!(case_skip_tools.is_match("CamelCaseHelper"));
2757 |         assert!(!case_skip_tools.is_match("camelCaseHelper"));
2758 | 
2759 |         // Test special_chars_plugin
2760 |         let special_plugin = &config.plugins[&PluginName("special_chars_plugin".to_string())];
2761 |         let special_skip_tools = special_plugin
2762 |             .runtime_config
2763 |             .as_ref()
2764 |             .unwrap()
2765 |             .skip_tools
2766 |             .as_ref()
2767 |             .unwrap();
2768 |         assert!(special_skip_tools.is_match("file.exe"));
2769 |         assert!(special_skip_tools.is_match("script?"));
2770 |         assert!(special_skip_tools.is_match("temp*data"));
2771 |         assert!(special_skip_tools.is_match("path\\tool"));
2772 |         assert!(!special_skip_tools.is_match("fileXexe"));
2773 |         assert!(!special_skip_tools.is_match("script"));
2774 | 
2775 |         // Test empty_skip_plugin
2776 |         let empty_plugin = &config.plugins[&PluginName("empty_skip_plugin".to_string())];
2777 |         let empty_skip_tools = empty_plugin
2778 |             .runtime_config
2779 |             .as_ref()
2780 |             .unwrap()
2781 |             .skip_tools
2782 |             .as_ref()
2783 |             .unwrap();
2784 |         assert_eq!(empty_skip_tools.len(), 0);
2785 |         assert!(!empty_skip_tools.is_match("anything"));
2786 | 
2787 |         // Test no_skip_plugin
2788 |         let no_skip_plugin = &config.plugins[&PluginName("no_skip_plugin".to_string())];
2789 |         assert!(
2790 |             no_skip_plugin
2791 |                 .runtime_config
2792 |                 .as_ref()
2793 |                 .unwrap()
2794 |                 .skip_tools
2795 |                 .is_none()
2796 |         );
2797 | 
2798 |         // Test full_config_plugin has all components
2799 |         let full_plugin = &config.plugins[&PluginName("full_config_plugin".to_string())];
2800 |         let full_runtime = full_plugin.runtime_config.as_ref().unwrap();
2801 |         let full_skip_tools = full_runtime.skip_tools.as_ref().unwrap();
2802 |         assert!(full_skip_tools.is_match("admin_tool"));
2803 |         assert!(full_skip_tools.is_match("tool_dangerous"));
2804 |         assert!(full_skip_tools.is_match("system_critical"));
2805 |         assert!(!full_skip_tools.is_match("safe_tool"));
2806 |         assert_eq!(full_runtime.allowed_hosts.as_ref().unwrap().len(), 2);
2807 |         assert_eq!(full_runtime.allowed_paths.as_ref().unwrap().len(), 2);
2808 |         assert_eq!(full_runtime.env_vars.as_ref().unwrap().len(), 2);
2809 |         assert_eq!(full_runtime.memory_limit.as_ref().unwrap(), "2GB");
2810 |     }
2811 | }
2812 | 
```
Page 9/11FirstPrevNextLast