osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
#[test]
fn parse_input_value_covers_float_unknown_and_allowed_scope_paths_unit() {
    let mut schema = ConfigSchema::default();
    schema.insert(
        "demo.float",
        super::SchemaEntry::float().with_doc("Demo floating-point value"),
    );

    assert_eq!(
        schema
            .parse_input_value("demo.float", "72.5")
            .expect("float should parse"),
        ConfigValue::Float(72.5)
    );
    assert!(matches!(
        schema
            .parse_input_value("demo.float", "wide")
            .expect_err("invalid float should fail"),
        crate::config::ConfigError::InvalidValueType {
            key,
            expected: SchemaValueType::Float,
            ..
        } if key == "demo.float"
    ));
    assert!(matches!(
        schema
            .parse_input_value("not.real", "x")
            .expect_err("unknown keys should fail"),
        crate::config::ConfigError::UnknownConfigKeys { keys } if keys == vec!["not.real".to_string()]
    ));
    assert_eq!(schema.doc_for_key("demo.float"), Some("Demo floating-point value"));
    assert_eq!(schema.doc_for_key("not.real"), None);

    validate_key_scope("profile.default", &Scope::terminal("repl"))
        .expect("bootstrap profile.default is allowed on terminal scope");
    validate_bootstrap_value("ui.format", &ConfigValue::String("json".to_string()))
        .expect("non-bootstrap values are ignored");
}

#[test]
fn schema_extension_runtime_and_scope_helpers_cover_aliases_and_bootstrap_specs_unit() {
    let mut schema = ConfigSchema::default();
    schema.set_allow_extensions_namespace(false);
    assert!(!schema.is_known_key("extensions.demo.token"));
    schema.set_allow_extensions_namespace(true);
    assert!(schema.is_known_key("extensions.demo.token"));
    assert!(schema.is_runtime_visible_key("extensions.demo.token"));
    assert!(schema.is_known_key("alias.lookup"));
    assert_eq!(
        schema
            .bootstrap_key_spec(" PROFILE.DEFAULT ")
            .map(|spec| spec.phase),
        Some(BootstrapPhase::Profile)
    );
    let env = parse_env_key("OSP__PROFILE__DEFAULT").expect("bootstrap env key should parse");
    assert_eq!(env.key, "profile.default");
    assert_eq!(env.scope, Scope::global());

    let env = parse_env_key("OSP__TERM__repl__PROFILE__DEFAULT")
        .expect("terminal-scoped bootstrap env key should parse");
    assert_eq!(env.key, "profile.default");
    assert_eq!(env.scope, Scope::terminal("repl"));

    assert!(bootstrap_key_spec("ui.format").is_none());
    assert!(!is_bootstrap_only_key("ui.format"));
}

#[test]
fn schema_adaptation_helpers_cover_scalar_secret_list_and_env_paths_unit() {
    let string_schema = super::SchemaEntry::string();
    let bool_schema = super::SchemaEntry::boolean();
    let int_schema = super::SchemaEntry::integer();
    let float_schema = super::SchemaEntry::float();
    let list_schema = super::SchemaEntry::string_list();

    assert_eq!(
        adapt_value_for_schema(
            "demo.string",
            &ConfigValue::Secret(SecretValue::new(ConfigValue::String("x".to_string()))),
            &string_schema
        )
        .expect("secret strings adapt"),
        ConfigValue::Secret(SecretValue::new(ConfigValue::String("x".to_string())))
    );
    assert_eq!(
        adapt_value_for_schema(
            "demo.bool",
            &ConfigValue::String("true".to_string()),
            &bool_schema
        )
        .expect("string bool adapts"),
        ConfigValue::Bool(true)
    );
    assert!(matches!(
        adapt_value_for_schema("demo.bool", &ConfigValue::Integer(1), &bool_schema)
            .expect_err("integer cannot adapt to bool"),
        crate::config::ConfigError::InvalidValueType { key, .. } if key == "demo.bool"
    ));
    assert_eq!(
        adapt_value_for_schema(
            "demo.int",
            &ConfigValue::String("7".to_string()),
            &int_schema
        )
        .expect("string int adapts"),
        ConfigValue::Integer(7)
    );
    assert_eq!(
        adapt_value_for_schema("demo.float", &ConfigValue::Integer(7), &float_schema)
            .expect("integer float adapts"),
        ConfigValue::Float(7.0)
    );
    assert!(matches!(
        adapt_value_for_schema("demo.float", &ConfigValue::Bool(true), &float_schema)
            .expect_err("bool cannot adapt to float"),
        crate::config::ConfigError::InvalidValueType { key, .. } if key == "demo.float"
    ));
    assert_eq!(
        adapt_value_for_schema(
            "demo.list",
            &ConfigValue::List(vec![
                ConfigValue::String("a".to_string()),
                ConfigValue::Secret(SecretValue::new(ConfigValue::String("b".to_string()))),
            ]),
            &list_schema
        )
        .expect("mixed string list adapts"),
        ConfigValue::List(vec![
            ConfigValue::String("a".to_string()),
            ConfigValue::String("b".to_string()),
        ])
    );
    assert!(matches!(
        adapt_value_for_schema(
            "demo.list",
            &ConfigValue::List(vec![ConfigValue::Integer(1)]),
            &list_schema
        )
        .expect_err("non-string list member should fail"),
        crate::config::ConfigError::InvalidValueType { key, .. } if key == "demo.list"
    ));
    assert_eq!(
        adapt_value_for_schema(
            "demo.secret_list",
            &ConfigValue::Secret(SecretValue::new(ConfigValue::String("a, b".to_string()))),
            &list_schema
        )
        .expect("secret string list adapts"),
        ConfigValue::Secret(SecretValue::new(ConfigValue::List(vec![
            ConfigValue::String("a".to_string()),
            ConfigValue::String("b".to_string()),
        ])))
    );

    assert_eq!(parse_string_list("['a', \"b\", c]"), vec!["a", "b", "c"]);
    assert!(parse_string_list("   ").is_empty());
    assert_eq!(
        value_type_name(&ConfigValue::Secret(SecretValue::new(ConfigValue::String(
            "x".into()
        )))),
        "string"
    );
    assert!(remaining_parts_are_bootstrap_profile_default(&[
        "PROFILE", "DEFAULT"
    ]));

    let env =
        parse_env_key("OSP__TERM__RePl__PROFILE__Ops__UI__FORMAT").expect("env key should parse");
    assert_eq!(env.key, "ui.format");
    assert_eq!(env.scope, Scope::profile_terminal("ops", "repl"));

    assert!(matches!(
        parse_env_key("NOT_OSP").err().expect("missing prefix should fail"),
        crate::config::ConfigError::InvalidEnvOverride { reason, .. } if reason.contains("missing OSP__ prefix")
    ));
    assert!(matches!(
        parse_env_key("OSP__TERM").err().expect("term requires name"),
        crate::config::ConfigError::InvalidEnvOverride { reason, .. } if reason.contains("TERM requires a terminal name")
    ));
    assert!(matches!(
        parse_env_key("OSP__PROFILE").err().expect("profile requires name"),
        crate::config::ConfigError::InvalidEnvOverride { reason, .. } if reason.contains("PROFILE requires a profile name")
    ));
    assert!(matches!(
        parse_env_key("OSP__TERM__a__TERM__b__UI__FORMAT")
            .err()
            .expect("duplicate TERM should fail"),
        crate::config::ConfigError::InvalidEnvOverride { reason, .. } if reason.contains("TERM scope specified more than once")
    ));
    assert!(matches!(
        parse_env_key("OSP__PROFILE__a__PROFILE__b__UI__FORMAT")
            .err()
            .expect("duplicate PROFILE should fail"),
        crate::config::ConfigError::InvalidEnvOverride { reason, .. } if reason.contains("PROFILE scope specified more than once")
    ));
    assert!(matches!(
        parse_env_key("OSP____").err().expect("missing key path should fail"),
        crate::config::ConfigError::InvalidEnvOverride { reason, .. } if reason.contains("missing key path")
    ));
    assert!(matches!(
        parse_env_key("OSP__TERM__repl")
            .err()
            .expect("missing final key should fail"),
        crate::config::ConfigError::InvalidEnvOverride { reason, .. } if reason.contains("missing final config key")
    ));

    let env = parse_env_key("OSP__PROFILE__DEFAULT").expect("bootstrap env key should parse");
    assert_eq!(env.key, "profile.default");
    assert_eq!(env.scope, Scope::global());

    let normalized = crate::config::normalize_scope(Scope {
        profile: Some("   ".to_string()),
        terminal: Some(" RePl ".to_string()),
    });
    assert_eq!(normalized.profile, None);
    assert_eq!(normalized.terminal.as_deref(), Some("repl"));

    assert!(!remaining_parts_are_bootstrap_profile_default(&[
        "PROFILE", "OPS"
    ]));
}

#[test]
fn adapt_value_for_schema_covers_float_and_secret_string_list_paths_unit() {
    assert_eq!(
        adapt_value_for_schema(
            "demo.float",
            &ConfigValue::Integer(4),
            &SchemaEntry::float()
        )
        .expect("integer should adapt to float"),
        ConfigValue::Float(4.0)
    );
    assert_eq!(
        adapt_value_for_schema(
            "demo.paths",
            &ConfigValue::String("base, custom".to_string()).into_secret(),
            &SchemaEntry::string_list(),
        )
        .expect("secret string should adapt into a secret string list"),
        ConfigValue::List(vec![
            ConfigValue::String("base".to_string()),
            ConfigValue::String("custom".to_string()),
        ])
        .into_secret()
    );

    let err = adapt_value_for_schema(
        "demo.paths",
        &ConfigValue::Integer(9).into_secret(),
        &SchemaEntry::string_list(),
    )
    .expect_err("secret non-string should fail");
    assert!(matches!(
        err,
        crate::config::ConfigError::InvalidValueType {
            key,
            expected: SchemaValueType::StringList,
            ..
        } if key == "demo.paths"
    ));
}