aperture_cli/
agent.rs

1use crate::cache::models::{
2    CachedApertureSecret, CachedCommand, CachedParameter, CachedRequestBody, CachedSpec,
3};
4use crate::config::models::GlobalConfig;
5use crate::config::url_resolver::BaseUrlResolver;
6use crate::error::Error;
7use openapiv3::{OpenAPI, Operation, Parameter as OpenApiParameter, ReferenceOr, SecurityScheme};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// JSON manifest describing all available commands and parameters for an API context.
12/// This is output when the `--describe-json` flag is used.
13#[derive(Debug, Serialize, Deserialize)]
14pub struct ApiCapabilityManifest {
15    /// Basic API metadata
16    pub api: ApiInfo,
17    /// Available command groups organized by tags
18    pub commands: HashMap<String, Vec<CommandInfo>>,
19    /// Security schemes available for this API
20    pub security_schemes: HashMap<String, SecuritySchemeInfo>,
21}
22
23#[derive(Debug, Serialize, Deserialize)]
24pub struct ApiInfo {
25    /// API name
26    pub name: String,
27    /// API version
28    pub version: String,
29    /// API description
30    pub description: Option<String>,
31    /// Base URL for the API
32    pub base_url: String,
33}
34
35#[derive(Debug, Serialize, Deserialize)]
36pub struct CommandInfo {
37    /// Command name (kebab-case operation ID)
38    pub name: String,
39    /// HTTP method
40    pub method: String,
41    /// API path with parameters
42    pub path: String,
43    /// Operation description
44    pub description: Option<String>,
45    /// Operation summary
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub summary: Option<String>,
48    /// Operation ID from the `OpenAPI` spec
49    pub operation_id: String,
50    /// Parameters for this operation
51    pub parameters: Vec<ParameterInfo>,
52    /// Request body information if applicable
53    pub request_body: Option<RequestBodyInfo>,
54    /// Security requirements for this operation
55    #[serde(skip_serializing_if = "Vec::is_empty", default)]
56    pub security_requirements: Vec<String>,
57    /// Tags associated with this operation
58    #[serde(skip_serializing_if = "Vec::is_empty", default)]
59    pub tags: Vec<String>,
60    /// Whether this operation is deprecated
61    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
62    pub deprecated: bool,
63    /// External documentation URL
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub external_docs_url: Option<String>,
66}
67
68#[derive(Debug, Serialize, Deserialize)]
69pub struct ParameterInfo {
70    /// Parameter name
71    pub name: String,
72    /// Parameter location (path, query, header)
73    pub location: String,
74    /// Whether the parameter is required
75    pub required: bool,
76    /// Parameter type
77    pub param_type: String,
78    /// Parameter description
79    pub description: Option<String>,
80    /// Parameter format (e.g., int32, int64, date-time)
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub format: Option<String>,
83    /// Default value if specified
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub default_value: Option<String>,
86    /// Enumeration of valid values
87    #[serde(skip_serializing_if = "Vec::is_empty", default)]
88    pub enum_values: Vec<String>,
89    /// Example value
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub example: Option<String>,
92}
93
94#[derive(Debug, Serialize, Deserialize)]
95pub struct RequestBodyInfo {
96    /// Whether the request body is required
97    pub required: bool,
98    /// Content type (e.g., "application/json")
99    pub content_type: String,
100    /// Description of the request body
101    pub description: Option<String>,
102    /// Example of the request body
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub example: Option<String>,
105}
106
107/// Detailed, parsable security scheme description
108#[derive(Debug, Serialize, Deserialize)]
109pub struct SecuritySchemeInfo {
110    /// Type of security scheme (http, apiKey)
111    #[serde(rename = "type")]
112    pub scheme_type: String,
113    /// Optional description of the security scheme
114    pub description: Option<String>,
115    /// Detailed scheme configuration
116    #[serde(flatten)]
117    pub details: SecuritySchemeDetails,
118    /// Aperture-specific secret mapping
119    #[serde(rename = "x-aperture-secret", skip_serializing_if = "Option::is_none")]
120    pub aperture_secret: Option<CachedApertureSecret>,
121}
122
123/// Detailed security scheme configuration
124#[derive(Debug, Serialize, Deserialize)]
125#[serde(tag = "scheme", rename_all = "camelCase")]
126pub enum SecuritySchemeDetails {
127    /// HTTP authentication scheme (e.g., bearer, basic)
128    #[serde(rename = "bearer")]
129    HttpBearer {
130        /// Optional bearer token format
131        #[serde(skip_serializing_if = "Option::is_none")]
132        bearer_format: Option<String>,
133    },
134    /// HTTP basic authentication
135    #[serde(rename = "basic")]
136    HttpBasic,
137    /// API Key authentication
138    #[serde(rename = "apiKey")]
139    ApiKey {
140        /// Location of the API key (header, query, cookie)
141        #[serde(rename = "in")]
142        location: String,
143        /// Name of the parameter/header
144        name: String,
145    },
146}
147
148/// Converts a kebab-case string from operationId to a CLI command name
149fn to_kebab_case(s: &str) -> String {
150    let mut result = String::new();
151    let mut prev_lowercase = false;
152
153    for (i, ch) in s.chars().enumerate() {
154        if ch.is_uppercase() && i > 0 && prev_lowercase {
155            result.push('-');
156        }
157        result.push(ch.to_ascii_lowercase());
158        prev_lowercase = ch.is_lowercase();
159    }
160
161    result
162}
163
164/// Generates a capability manifest from an `OpenAPI` specification.
165///
166/// This function creates a comprehensive JSON description of all available commands,
167/// parameters, and security requirements directly from the original `OpenAPI` spec,
168/// preserving all metadata that might be lost in the cached representation.
169///
170/// # Arguments
171/// * `api_name` - The name of the API context
172/// * `spec` - The original `OpenAPI` specification
173/// * `global_config` - Optional global configuration for URL resolution
174///
175/// # Returns
176/// * `Ok(String)` - JSON-formatted capability manifest
177/// * `Err(Error)` - If JSON serialization fails
178///
179/// # Errors
180/// Returns an error if JSON serialization fails
181pub fn generate_capability_manifest_from_openapi(
182    api_name: &str,
183    spec: &OpenAPI,
184    global_config: Option<&GlobalConfig>,
185) -> Result<String, Error> {
186    // First, convert the OpenAPI spec to a temporary CachedSpec for URL resolution
187    let base_url = spec.servers.first().map(|s| s.url.clone());
188    let servers: Vec<String> = spec.servers.iter().map(|s| s.url.clone()).collect();
189
190    let temp_cached_spec = CachedSpec {
191        cache_format_version: crate::cache::models::CACHE_FORMAT_VERSION,
192        name: api_name.to_string(),
193        version: spec.info.version.clone(),
194        commands: vec![], // We'll generate commands directly from OpenAPI
195        base_url,
196        servers,
197        security_schemes: HashMap::new(), // We'll extract these directly too
198    };
199
200    // Resolve base URL using the same priority hierarchy as executor
201    let resolver = BaseUrlResolver::new(&temp_cached_spec);
202    let resolver = if let Some(config) = global_config {
203        resolver.with_global_config(config)
204    } else {
205        resolver
206    };
207    let resolved_base_url = resolver.resolve(None);
208
209    // Extract commands directly from OpenAPI spec
210    let mut command_groups: HashMap<String, Vec<CommandInfo>> = HashMap::new();
211
212    for (path, path_item) in &spec.paths.paths {
213        if let ReferenceOr::Item(item) = path_item {
214            // Process each HTTP method
215            for (method, operation) in crate::spec::http_methods_iter(item) {
216                if let Some(op) = operation {
217                    let command_info =
218                        convert_openapi_operation_to_info(method, path, op, spec.security.as_ref());
219
220                    // Group by first tag or "default"
221                    let group_name = op
222                        .tags
223                        .first()
224                        .cloned()
225                        .unwrap_or_else(|| "default".to_string());
226
227                    command_groups
228                        .entry(group_name)
229                        .or_default()
230                        .push(command_info);
231                }
232            }
233        }
234    }
235
236    // Extract security schemes directly from OpenAPI
237    let security_schemes = extract_security_schemes_from_openapi(spec);
238
239    // Create the manifest
240    let manifest = ApiCapabilityManifest {
241        api: ApiInfo {
242            name: spec.info.title.clone(),
243            version: spec.info.version.clone(),
244            description: spec.info.description.clone(),
245            base_url: resolved_base_url,
246        },
247        commands: command_groups,
248        security_schemes,
249    };
250
251    // Serialize to JSON
252    serde_json::to_string_pretty(&manifest).map_err(Error::Json)
253}
254
255/// Generates a capability manifest from a cached API specification.
256///
257/// This function creates a comprehensive JSON description of all available commands,
258/// parameters, and security requirements for the given API context.
259///
260/// # Arguments
261/// * `spec` - The cached API specification
262/// * `global_config` - Optional global configuration for URL resolution
263///
264/// # Returns
265/// * `Ok(String)` - JSON-formatted capability manifest
266/// * `Err(Error)` - If JSON serialization fails
267///
268/// # Errors
269/// Returns an error if JSON serialization fails
270pub fn generate_capability_manifest(
271    spec: &CachedSpec,
272    global_config: Option<&GlobalConfig>,
273) -> Result<String, Error> {
274    let mut command_groups: HashMap<String, Vec<CommandInfo>> = HashMap::new();
275
276    // Group commands by their tag (namespace) and convert to CommandInfo
277    for cached_command in &spec.commands {
278        let group_name = if cached_command.name.is_empty() {
279            "default".to_string()
280        } else {
281            cached_command.name.clone()
282        };
283
284        let command_info = convert_cached_command_to_info(cached_command);
285        command_groups
286            .entry(group_name)
287            .or_default()
288            .push(command_info);
289    }
290
291    // Resolve base URL using the same priority hierarchy as executor
292    let resolver = BaseUrlResolver::new(spec);
293    let resolver = if let Some(config) = global_config {
294        resolver.with_global_config(config)
295    } else {
296        resolver
297    };
298    let base_url = resolver.resolve(None);
299
300    // Create the manifest
301    let manifest = ApiCapabilityManifest {
302        api: ApiInfo {
303            name: spec.name.clone(),
304            version: spec.version.clone(),
305            description: None, // Not available in cached spec
306            base_url,
307        },
308        commands: command_groups,
309        security_schemes: extract_security_schemes(spec),
310    };
311
312    // Serialize to JSON
313    serde_json::to_string_pretty(&manifest).map_err(Error::Json)
314}
315
316/// Converts a `CachedCommand` to `CommandInfo` for the manifest
317fn convert_cached_command_to_info(cached_command: &CachedCommand) -> CommandInfo {
318    let command_name = if cached_command.operation_id.is_empty() {
319        cached_command.method.to_lowercase()
320    } else {
321        to_kebab_case(&cached_command.operation_id)
322    };
323
324    let parameters: Vec<ParameterInfo> = cached_command
325        .parameters
326        .iter()
327        .map(convert_cached_parameter_to_info)
328        .collect();
329
330    let request_body = cached_command
331        .request_body
332        .as_ref()
333        .map(convert_cached_request_body_to_info);
334
335    CommandInfo {
336        name: command_name,
337        method: cached_command.method.clone(),
338        path: cached_command.path.clone(),
339        description: cached_command.description.clone(),
340        summary: cached_command.summary.clone(),
341        operation_id: cached_command.operation_id.clone(),
342        parameters,
343        request_body,
344        security_requirements: cached_command.security_requirements.clone(),
345        tags: cached_command.tags.clone(),
346        deprecated: cached_command.deprecated,
347        external_docs_url: cached_command.external_docs_url.clone(),
348    }
349}
350
351/// Converts a `CachedParameter` to `ParameterInfo` for the manifest
352fn convert_cached_parameter_to_info(cached_param: &CachedParameter) -> ParameterInfo {
353    ParameterInfo {
354        name: cached_param.name.clone(),
355        location: cached_param.location.clone(),
356        required: cached_param.required,
357        param_type: cached_param
358            .schema_type
359            .clone()
360            .unwrap_or_else(|| "string".to_string()),
361        description: cached_param.description.clone(),
362        format: cached_param.format.clone(),
363        default_value: cached_param.default_value.clone(),
364        enum_values: cached_param.enum_values.clone(),
365        example: cached_param.example.clone(),
366    }
367}
368
369/// Converts a `CachedRequestBody` to `RequestBodyInfo` for the manifest
370fn convert_cached_request_body_to_info(cached_body: &CachedRequestBody) -> RequestBodyInfo {
371    RequestBodyInfo {
372        required: cached_body.required,
373        content_type: cached_body.content_type.clone(),
374        description: cached_body.description.clone(),
375        example: cached_body.example.clone(),
376    }
377}
378
379/// Extracts security schemes from the cached spec for the capability manifest
380fn extract_security_schemes(spec: &CachedSpec) -> HashMap<String, SecuritySchemeInfo> {
381    let mut security_schemes = HashMap::new();
382
383    for (name, scheme) in &spec.security_schemes {
384        let details = match scheme.scheme_type.as_str() {
385            "http" => {
386                scheme.scheme.as_ref().map_or(
387                    SecuritySchemeDetails::HttpBearer {
388                        bearer_format: None,
389                    },
390                    |http_scheme| match http_scheme.as_str() {
391                        "bearer" => SecuritySchemeDetails::HttpBearer {
392                            bearer_format: scheme.bearer_format.clone(),
393                        },
394                        "basic" => SecuritySchemeDetails::HttpBasic,
395                        _ => {
396                            // For other HTTP schemes, default to bearer
397                            SecuritySchemeDetails::HttpBearer {
398                                bearer_format: None,
399                            }
400                        }
401                    },
402                )
403            }
404            "apiKey" => SecuritySchemeDetails::ApiKey {
405                location: scheme
406                    .location
407                    .clone()
408                    .unwrap_or_else(|| "header".to_string()),
409                name: scheme
410                    .parameter_name
411                    .clone()
412                    .unwrap_or_else(|| "Authorization".to_string()),
413            },
414            _ => {
415                // Default to bearer for unknown types
416                SecuritySchemeDetails::HttpBearer {
417                    bearer_format: None,
418                }
419            }
420        };
421
422        let scheme_info = SecuritySchemeInfo {
423            scheme_type: scheme.scheme_type.clone(),
424            description: scheme.description.clone(),
425            details,
426            aperture_secret: scheme.aperture_secret.clone(),
427        };
428
429        security_schemes.insert(name.clone(), scheme_info);
430    }
431
432    security_schemes
433}
434
435/// Converts an `OpenAPI` operation to `CommandInfo` with full metadata
436fn convert_openapi_operation_to_info(
437    method: &str,
438    path: &str,
439    operation: &Operation,
440    global_security: Option<&Vec<openapiv3::SecurityRequirement>>,
441) -> CommandInfo {
442    let command_name = operation
443        .operation_id
444        .as_ref()
445        .map_or_else(|| method.to_lowercase(), |op_id| to_kebab_case(op_id));
446
447    // Extract parameters with full metadata
448    let parameters: Vec<ParameterInfo> = operation
449        .parameters
450        .iter()
451        .filter_map(|param_ref| {
452            if let ReferenceOr::Item(param) = param_ref {
453                Some(convert_openapi_parameter_to_info(param))
454            } else {
455                None
456            }
457        })
458        .collect();
459
460    // Extract request body info
461    let request_body = operation.request_body.as_ref().and_then(|rb_ref| {
462        if let ReferenceOr::Item(body) = rb_ref {
463            // Prefer JSON content if available
464            let content_type = if body.content.contains_key("application/json") {
465                "application/json"
466            } else {
467                body.content.keys().next().map(String::as_str)?
468            };
469
470            let media_type = body.content.get(content_type)?;
471            let example = media_type
472                .example
473                .as_ref()
474                .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()));
475
476            Some(RequestBodyInfo {
477                required: body.required,
478                content_type: content_type.to_string(),
479                description: body.description.clone(),
480                example,
481            })
482        } else {
483            None
484        }
485    });
486
487    // Extract security requirements
488    let security_requirements = operation.security.as_ref().map_or_else(
489        || {
490            global_security.map_or(vec![], |reqs| {
491                reqs.iter().flat_map(|req| req.keys().cloned()).collect()
492            })
493        },
494        |op_security| {
495            op_security
496                .iter()
497                .flat_map(|req| req.keys().cloned())
498                .collect()
499        },
500    );
501
502    CommandInfo {
503        name: command_name,
504        method: method.to_uppercase(),
505        path: path.to_string(),
506        description: operation.description.clone(),
507        summary: operation.summary.clone(),
508        operation_id: operation.operation_id.clone().unwrap_or_default(),
509        parameters,
510        request_body,
511        security_requirements,
512        tags: operation.tags.clone(),
513        deprecated: operation.deprecated,
514        external_docs_url: operation
515            .external_docs
516            .as_ref()
517            .map(|docs| docs.url.clone()),
518    }
519}
520
521/// Converts an `OpenAPI` parameter to `ParameterInfo` with full metadata
522fn convert_openapi_parameter_to_info(param: &OpenApiParameter) -> ParameterInfo {
523    let (param_data, location_str) = match param {
524        OpenApiParameter::Query { parameter_data, .. } => (parameter_data, "query"),
525        OpenApiParameter::Header { parameter_data, .. } => (parameter_data, "header"),
526        OpenApiParameter::Path { parameter_data, .. } => (parameter_data, "path"),
527        OpenApiParameter::Cookie { parameter_data, .. } => (parameter_data, "cookie"),
528    };
529
530    // Extract schema information
531    let (schema_type, format, default_value, enum_values, example) =
532        if let openapiv3::ParameterSchemaOrContent::Schema(schema_ref) = &param_data.format {
533            match schema_ref {
534                ReferenceOr::Item(schema) => {
535                    let (schema_type, format, enums) = match &schema.schema_kind {
536                        openapiv3::SchemaKind::Type(type_val) => match type_val {
537                            openapiv3::Type::String(string_type) => {
538                                let enum_values: Vec<String> = string_type
539                                    .enumeration
540                                    .iter()
541                                    .filter_map(|v| v.as_ref())
542                                    .map(|v| {
543                                        serde_json::to_string(v).unwrap_or_else(|_| v.to_string())
544                                    })
545                                    .collect();
546                                ("string".to_string(), None, enum_values)
547                            }
548                            openapiv3::Type::Number(_) => ("number".to_string(), None, vec![]),
549                            openapiv3::Type::Integer(_) => ("integer".to_string(), None, vec![]),
550                            openapiv3::Type::Boolean(_) => ("boolean".to_string(), None, vec![]),
551                            openapiv3::Type::Array(_) => ("array".to_string(), None, vec![]),
552                            openapiv3::Type::Object(_) => ("object".to_string(), None, vec![]),
553                        },
554                        _ => ("string".to_string(), None, vec![]),
555                    };
556
557                    let default_value = schema
558                        .schema_data
559                        .default
560                        .as_ref()
561                        .map(|v| serde_json::to_string(v).unwrap_or_else(|_| v.to_string()));
562
563                    (Some(schema_type), format, default_value, enums, None)
564                }
565                ReferenceOr::Reference { .. } => {
566                    (Some("string".to_string()), None, None, vec![], None)
567                }
568            }
569        } else {
570            (Some("string".to_string()), None, None, vec![], None)
571        };
572
573    // Extract example from parameter data
574    let example = param_data
575        .example
576        .as_ref()
577        .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()))
578        .or(example);
579
580    ParameterInfo {
581        name: param_data.name.clone(),
582        location: location_str.to_string(),
583        required: param_data.required,
584        param_type: schema_type.unwrap_or_else(|| "string".to_string()),
585        description: param_data.description.clone(),
586        format,
587        default_value,
588        enum_values,
589        example,
590    }
591}
592
593/// Extracts security schemes directly from `OpenAPI` spec
594fn extract_security_schemes_from_openapi(spec: &OpenAPI) -> HashMap<String, SecuritySchemeInfo> {
595    let mut security_schemes = HashMap::new();
596
597    if let Some(components) = &spec.components {
598        for (name, scheme_ref) in &components.security_schemes {
599            if let ReferenceOr::Item(scheme) = scheme_ref {
600                if let Some(scheme_info) = convert_openapi_security_scheme(name, scheme) {
601                    security_schemes.insert(name.clone(), scheme_info);
602                }
603            }
604        }
605    }
606
607    security_schemes
608}
609
610/// Converts an `OpenAPI` security scheme to `SecuritySchemeInfo`
611fn convert_openapi_security_scheme(
612    _name: &str,
613    scheme: &SecurityScheme,
614) -> Option<SecuritySchemeInfo> {
615    match scheme {
616        SecurityScheme::APIKey {
617            location,
618            name: param_name,
619            description,
620            ..
621        } => {
622            let location_str = match location {
623                openapiv3::APIKeyLocation::Query => "query",
624                openapiv3::APIKeyLocation::Header => "header",
625                openapiv3::APIKeyLocation::Cookie => "cookie",
626            };
627
628            let aperture_secret = extract_aperture_secret_from_extensions(scheme);
629
630            Some(SecuritySchemeInfo {
631                scheme_type: "apiKey".to_string(),
632                description: description.clone(),
633                details: SecuritySchemeDetails::ApiKey {
634                    location: location_str.to_string(),
635                    name: param_name.clone(),
636                },
637                aperture_secret,
638            })
639        }
640        SecurityScheme::HTTP {
641            scheme: http_scheme,
642            bearer_format,
643            description,
644            ..
645        } => {
646            let details = match http_scheme.as_str() {
647                "bearer" => SecuritySchemeDetails::HttpBearer {
648                    bearer_format: bearer_format.clone(),
649                },
650                "basic" => SecuritySchemeDetails::HttpBasic,
651                _ => SecuritySchemeDetails::HttpBearer {
652                    bearer_format: None,
653                },
654            };
655
656            let aperture_secret = extract_aperture_secret_from_extensions(scheme);
657
658            Some(SecuritySchemeInfo {
659                scheme_type: "http".to_string(),
660                description: description.clone(),
661                details,
662                aperture_secret,
663            })
664        }
665        SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => None,
666    }
667}
668
669/// Extracts x-aperture-secret extension from a security scheme's extensions
670fn extract_aperture_secret_from_extensions(
671    scheme: &SecurityScheme,
672) -> Option<CachedApertureSecret> {
673    let extensions = match scheme {
674        SecurityScheme::APIKey { extensions, .. } | SecurityScheme::HTTP { extensions, .. } => {
675            extensions
676        }
677        SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => return None,
678    };
679
680    extensions.get("x-aperture-secret").and_then(|value| {
681        if let Some(obj) = value.as_object() {
682            let source = obj.get("source")?.as_str()?;
683            let name = obj.get("name")?.as_str()?;
684
685            if source == "env" {
686                return Some(CachedApertureSecret {
687                    source: source.to_string(),
688                    name: name.to_string(),
689                });
690            }
691        }
692        None
693    })
694}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699    use crate::cache::models::{CachedCommand, CachedParameter, CachedSpec};
700
701    #[test]
702    fn test_to_kebab_case() {
703        assert_eq!(to_kebab_case("getUserById"), "get-user-by-id");
704        assert_eq!(to_kebab_case("createUser"), "create-user");
705        assert_eq!(to_kebab_case("list"), "list");
706        assert_eq!(to_kebab_case("GET"), "get");
707    }
708
709    #[test]
710    fn test_generate_capability_manifest() {
711        use crate::cache::models::{CachedApertureSecret, CachedSecurityScheme};
712
713        let mut security_schemes = HashMap::new();
714        security_schemes.insert(
715            "bearerAuth".to_string(),
716            CachedSecurityScheme {
717                name: "bearerAuth".to_string(),
718                scheme_type: "http".to_string(),
719                scheme: Some("bearer".to_string()),
720                location: Some("header".to_string()),
721                parameter_name: Some("Authorization".to_string()),
722                description: None,
723                bearer_format: None,
724                aperture_secret: Some(CachedApertureSecret {
725                    source: "env".to_string(),
726                    name: "API_TOKEN".to_string(),
727                }),
728            },
729        );
730
731        let spec = CachedSpec {
732            cache_format_version: crate::cache::models::CACHE_FORMAT_VERSION,
733            name: "Test API".to_string(),
734            version: "1.0.0".to_string(),
735            commands: vec![CachedCommand {
736                name: "users".to_string(),
737                description: Some("Get user by ID".to_string()),
738                summary: None,
739                operation_id: "getUserById".to_string(),
740                method: "GET".to_string(),
741                path: "/users/{id}".to_string(),
742                parameters: vec![CachedParameter {
743                    name: "id".to_string(),
744                    location: "path".to_string(),
745                    required: true,
746                    description: None,
747                    schema: Some("string".to_string()),
748                    schema_type: Some("string".to_string()),
749                    format: None,
750                    default_value: None,
751                    enum_values: vec![],
752                    example: None,
753                }],
754                request_body: None,
755                responses: vec![],
756                security_requirements: vec!["bearerAuth".to_string()],
757                tags: vec!["users".to_string()],
758                deprecated: false,
759                external_docs_url: None,
760            }],
761            base_url: Some("https://test-api.example.com".to_string()),
762            servers: vec!["https://test-api.example.com".to_string()],
763            security_schemes,
764        };
765
766        let manifest_json = generate_capability_manifest(&spec, None).unwrap();
767        let manifest: ApiCapabilityManifest = serde_json::from_str(&manifest_json).unwrap();
768
769        assert_eq!(manifest.api.name, "Test API");
770        assert_eq!(manifest.api.version, "1.0.0");
771        assert!(manifest.commands.contains_key("users"));
772
773        let users_commands = &manifest.commands["users"];
774        assert_eq!(users_commands.len(), 1);
775        assert_eq!(users_commands[0].name, "get-user-by-id");
776        assert_eq!(users_commands[0].method, "GET");
777        assert_eq!(users_commands[0].parameters.len(), 1);
778        assert_eq!(users_commands[0].parameters[0].name, "id");
779
780        // Test security information extraction
781        assert!(!manifest.security_schemes.is_empty());
782        assert!(manifest.security_schemes.contains_key("bearerAuth"));
783        let bearer_auth = &manifest.security_schemes["bearerAuth"];
784        assert_eq!(bearer_auth.scheme_type, "http");
785        assert!(matches!(
786            &bearer_auth.details,
787            SecuritySchemeDetails::HttpBearer { .. }
788        ));
789        assert!(bearer_auth.aperture_secret.is_some());
790        let aperture_secret = bearer_auth.aperture_secret.as_ref().unwrap();
791        assert_eq!(aperture_secret.name, "API_TOKEN");
792        assert_eq!(aperture_secret.source, "env");
793    }
794}