Skip to content

Commit ce5f38f

Browse files
committed
test(coverage): batch 5.4 — config/{schemas, schema/observability}
- config/schemas.rs: 52.37% (adds required_string / optional_string / optional_bool field builder coverage, exercises deserialize_params across ModelSettings / MemorySettings / WorkspaceOnboarding / SetBrowserAllowAll / OnboardingCompleted params, pins DEFAULT_ONBOARDING_FLAG_NAME constant) - config/schema/observability.rs: 66.67% (default-values invariant, serde defaults for optional fields, explicit analytics flag, round-trip) 16 new deterministic tests. Refs tinyhumansai#530.
1 parent 0873760 commit ce5f38f

2 files changed

Lines changed: 173 additions & 0 deletions

File tree

‎src/openhuman/config/schema/observability.rs‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,63 @@ impl Default for ObservabilityConfig {
4141
}
4242
}
4343
}
44+
45+
#[cfg(test)]
46+
mod tests {
47+
use super::*;
48+
use serde_json::json;
49+
50+
#[test]
51+
fn default_disables_backend_and_enables_analytics() {
52+
let cfg = ObservabilityConfig::default();
53+
assert_eq!(cfg.backend, "none");
54+
assert!(cfg.otel_endpoint.is_none());
55+
assert!(cfg.otel_service_name.is_none());
56+
assert!(cfg.sentry_dsn.is_none());
57+
assert!(cfg.analytics_enabled);
58+
}
59+
60+
#[test]
61+
fn default_analytics_enabled_helper_returns_true() {
62+
assert!(default_analytics_enabled());
63+
}
64+
65+
#[test]
66+
fn deserialize_missing_optional_fields_uses_defaults() {
67+
let cfg: ObservabilityConfig = serde_json::from_value(json!({
68+
"backend": "log"
69+
}))
70+
.unwrap();
71+
assert_eq!(cfg.backend, "log");
72+
assert!(cfg.otel_endpoint.is_none());
73+
assert!(cfg.analytics_enabled, "analytics default must be true");
74+
}
75+
76+
#[test]
77+
fn deserialize_respects_explicit_analytics_flag() {
78+
let cfg: ObservabilityConfig = serde_json::from_value(json!({
79+
"backend": "otel",
80+
"analytics_enabled": false
81+
}))
82+
.unwrap();
83+
assert!(!cfg.analytics_enabled);
84+
}
85+
86+
#[test]
87+
fn round_trip_preserves_all_fields() {
88+
let original = ObservabilityConfig {
89+
backend: "otel".into(),
90+
otel_endpoint: Some("http://localhost:4318".into()),
91+
otel_service_name: Some("openhuman-test".into()),
92+
sentry_dsn: Some("https://token@sentry.io/1".into()),
93+
analytics_enabled: false,
94+
};
95+
let s = serde_json::to_string(&original).unwrap();
96+
let back: ObservabilityConfig = serde_json::from_str(&s).unwrap();
97+
assert_eq!(back.backend, "otel");
98+
assert_eq!(back.otel_endpoint.as_deref(), Some("http://localhost:4318"));
99+
assert_eq!(back.otel_service_name.as_deref(), Some("openhuman-test"));
100+
assert!(back.sentry_dsn.is_some());
101+
assert!(!back.analytics_enabled);
102+
}
103+
}

‎src/openhuman/config/schemas.rs‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,4 +870,117 @@ mod tests {
870870
.expect("serialize");
871871
assert!(v.get("logs").is_some() || v.get("result").is_some());
872872
}
873+
874+
// ── Field builder helpers ────────────────────────────────────
875+
876+
#[test]
877+
fn required_string_builds_required_string_field() {
878+
let f = required_string("api_key", "Auth key");
879+
assert_eq!(f.name, "api_key");
880+
assert_eq!(f.comment, "Auth key");
881+
assert!(f.required);
882+
assert!(matches!(f.ty, TypeSchema::String));
883+
}
884+
885+
#[test]
886+
fn optional_string_builds_option_string_field() {
887+
let f = optional_string("model", "model name");
888+
assert!(!f.required);
889+
match &f.ty {
890+
TypeSchema::Option(inner) => assert!(matches!(**inner, TypeSchema::String)),
891+
other => panic!("expected Option<String>, got {other:?}"),
892+
}
893+
}
894+
895+
#[test]
896+
fn optional_bool_builds_option_bool_field() {
897+
let f = optional_bool("enabled", "Whether enabled");
898+
assert!(!f.required);
899+
match &f.ty {
900+
TypeSchema::Option(inner) => assert!(matches!(**inner, TypeSchema::Bool)),
901+
other => panic!("expected Option<Bool>, got {other:?}"),
902+
}
903+
}
904+
905+
// ── deserialize_params helper ────────────────────────────────
906+
907+
#[test]
908+
fn deserialize_params_parses_model_settings_update() {
909+
let mut m = Map::new();
910+
m.insert("api_key".into(), Value::String("sk-123".into()));
911+
m.insert(
912+
"default_temperature".into(),
913+
Value::Number(serde_json::Number::from_f64(0.7).unwrap()),
914+
);
915+
let out: ModelSettingsUpdate = deserialize_params(m).unwrap();
916+
assert_eq!(out.api_key.as_deref(), Some("sk-123"));
917+
assert_eq!(out.default_temperature, Some(0.7));
918+
assert!(out.api_url.is_none());
919+
assert!(out.default_model.is_none());
920+
}
921+
922+
#[test]
923+
fn deserialize_params_parses_memory_settings_update() {
924+
let mut m = Map::new();
925+
m.insert("backend".into(), Value::String("sqlite".into()));
926+
m.insert("auto_save".into(), Value::Bool(true));
927+
m.insert(
928+
"embedding_dimensions".into(),
929+
Value::Number(serde_json::Number::from(1536)),
930+
);
931+
let out: MemorySettingsUpdate = deserialize_params(m).unwrap();
932+
assert_eq!(out.backend.as_deref(), Some("sqlite"));
933+
assert_eq!(out.auto_save, Some(true));
934+
assert_eq!(out.embedding_dimensions, Some(1536));
935+
}
936+
937+
#[test]
938+
fn deserialize_params_parses_workspace_onboarding_flag_params() {
939+
let out: WorkspaceOnboardingFlagParams = deserialize_params(Map::new()).unwrap();
940+
assert!(out.flag_name.is_none());
941+
942+
let mut m = Map::new();
943+
m.insert("flag_name".into(), Value::String(".custom_marker".into()));
944+
let out: WorkspaceOnboardingFlagParams = deserialize_params(m).unwrap();
945+
assert_eq!(out.flag_name.as_deref(), Some(".custom_marker"));
946+
}
947+
948+
#[test]
949+
fn deserialize_params_parses_workspace_onboarding_flag_set_params() {
950+
let mut m = Map::new();
951+
m.insert("value".into(), Value::Bool(true));
952+
let out: WorkspaceOnboardingFlagSetParams = deserialize_params(m).unwrap();
953+
assert_eq!(out.value, true);
954+
assert!(out.flag_name.is_none());
955+
}
956+
957+
#[test]
958+
fn deserialize_params_rejects_wrong_types_with_invalid_params_prefix() {
959+
let mut m = Map::new();
960+
m.insert(
961+
"default_temperature".into(),
962+
Value::String("not-a-number".into()),
963+
);
964+
let err = deserialize_params::<ModelSettingsUpdate>(m).unwrap_err();
965+
assert!(err.starts_with("invalid params"));
966+
}
967+
968+
#[test]
969+
fn deserialize_params_requires_value_on_set_onboarding() {
970+
let err = deserialize_params::<OnboardingCompletedSetParams>(Map::new()).unwrap_err();
971+
assert!(err.contains("invalid params"));
972+
}
973+
974+
#[test]
975+
fn deserialize_params_rejects_missing_required_for_set_browser_allow_all() {
976+
let err = deserialize_params::<SetBrowserAllowAllParams>(Map::new()).unwrap_err();
977+
assert!(err.contains("invalid params"));
978+
}
979+
980+
#[test]
981+
fn default_onboarding_flag_constant_points_to_hidden_marker() {
982+
// Keeps the constant's observable value pinned so tool behavior
983+
// stays stable across refactors.
984+
assert_eq!(DEFAULT_ONBOARDING_FLAG_NAME, ".skip_onboarding");
985+
}
873986
}

0 commit comments

Comments
 (0)