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