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