use super::super::models_dev::{
MODELS_DEV_BODY_MAX_BYTES, ModelsDevFetchError, ModelsDevLookup, SharedModelsDevLookup,
enrich_custom_provider_catalog_with_models_dev_value, enrich_with_models_dev_namespace,
explicit_reasoning_efforts, fetch_models_dev_catalog_with_timeouts, models_dev_source,
supports_reasoning,
};
use super::super::{
CachePreference, CatalogRefreshFailureCategory, CatalogSource, ModelCatalogEntry,
load_custom_provider_catalog_with_models_dev,
};
use crate::{config::CustomProviderConfig, thinking::ThinkingLevel};
use std::time::Duration;
use tempfile::TempDir;
#[test]
fn model_catalog_parses_current_reasoning_options_and_none_as_default() {
let model = serde_json::json!({
"reasoning": true,
"reasoning_options": [{"type": "effort", "values": ["none", "low", "high", "high", "unknown", "xhigh", "max"]}]
});
assert_eq!(
explicit_reasoning_efforts(&model),
Some(vec![
ThinkingLevel::Default,
ThinkingLevel::Low,
ThinkingLevel::High,
ThinkingLevel::XHigh,
ThinkingLevel::Max
])
);
assert!(supports_reasoning(&model));
assert_eq!(
explicit_reasoning_efforts(&serde_json::json!({"reasoning_options": []})),
None
);
}
#[test]
fn current_reasoning_options_require_explicit_reasoning_capability() {
for reasoning in [None, Some(false)] {
let mut model = serde_json::json!({
"reasoning_options": [{"type": "effort", "values": ["low", "high"]}]
});
if let Some(reasoning) = reasoning {
model["reasoning"] = serde_json::Value::Bool(reasoning);
}
assert_eq!(explicit_reasoning_efforts(&model), None);
}
assert_eq!(
explicit_reasoning_efforts(&serde_json::json!({
"reasoning": true,
"reasoning_options": [{"type": "effort", "values": ["low", "high"]}]
})),
Some(vec![
ThinkingLevel::Default,
ThinkingLevel::Low,
ThinkingLevel::High
])
);
}
#[test]
fn legacy_reasoning_efforts_remain_explicit_compatibility_path() {
assert_eq!(
explicit_reasoning_efforts(&serde_json::json!({
"reasoning": {"efforts": ["low", "high"]}
})),
Some(vec![
ThinkingLevel::Default,
ThinkingLevel::Low,
ThinkingLevel::High
])
);
}
#[test]
fn model_catalog_parses_legacy_reasoning_efforts_in_canonical_order() {
let model = serde_json::json!({"reasoning": {"efforts": ["max", "unknown", "high", "high"]}});
assert_eq!(
explicit_reasoning_efforts(&model),
Some(vec![
ThinkingLevel::Default,
ThinkingLevel::High,
ThinkingLevel::Max
])
);
let model = serde_json::json!({"reasoning": {"efforts": ["medium", "low", "high"]}});
assert_eq!(
explicit_reasoning_efforts(&model),
Some(ThinkingLevel::EFFORT_GENERIC.to_vec())
);
assert_eq!(
explicit_reasoning_efforts(&serde_json::json!({"reasoning": {"efforts": ["unknown"]}})),
None
);
}
#[test]
fn model_catalog_reasoning_booleans_mark_support_without_synthesizing_efforts() {
for model in [
serde_json::json!({"reasoning": {"effort": true}}),
serde_json::json!({"reasoning": true}),
] {
assert_eq!(explicit_reasoning_efforts(&model), None);
assert!(supports_reasoning(&model));
}
}
#[test]
fn models_dev_fetch_uses_observable_request_timeout() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
let handle = std::thread::spawn(move || {
let _accepted = listener.accept();
std::thread::sleep(Duration::from_secs(2));
});
let started = std::time::Instant::now();
let error = fetch_models_dev_catalog_with_timeouts(
&format!("http://{address}/api.json"),
Duration::from_millis(100),
Duration::from_millis(100),
)
.unwrap_err();
assert!(started.elapsed() < Duration::from_secs(1));
assert_eq!(error, ModelsDevFetchError::Timeout);
handle.join().unwrap();
}
#[test]
fn models_dev_fetch_rejects_oversized_success_body() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
let body = format!(
r#"{{"openai":{{"models":{{}}}},"padding":"{}"}}"#,
"x".repeat(MODELS_DEV_BODY_MAX_BYTES as usize + 1)
);
let handle = std::thread::spawn(move || {
use std::io::{Read, Write};
let (mut stream, _) = listener.accept().unwrap();
let mut request = [0_u8; 1024];
let _ = stream.read(&mut request);
write!(
stream,
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
body.len(),
body
)
.unwrap();
});
let error = fetch_models_dev_catalog_with_timeouts(
&format!("http://{address}/api.json"),
Duration::from_millis(100),
Duration::from_secs(5),
)
.unwrap_err();
handle.join().unwrap();
assert_eq!(error, ModelsDevFetchError::BodyLimit);
}
#[test]
fn models_dev_fetch_accepts_body_just_under_source_specific_limit() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
let body = format!(
r#"{{"openai":{{"models":{{}}}},"padding":"{}"}}"#,
"x".repeat(MODELS_DEV_BODY_MAX_BYTES as usize - 128)
);
let handle = std::thread::spawn(move || {
use std::io::{Read, Write};
let (mut stream, _) = listener.accept().unwrap();
let mut request = [0_u8; 1024];
let _ = stream.read(&mut request);
write!(
stream,
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
body.len(), body
)
.unwrap();
});
let value = fetch_models_dev_catalog_with_timeouts(
&format!("http://{address}/api.json"),
Duration::from_millis(100),
Duration::from_secs(5),
)
.unwrap();
handle.join().unwrap();
assert_eq!(value["openai"]["models"], serde_json::json!({}));
}
#[test]
fn models_dev_fetch_rejects_http_error_before_decoding_json() {
let (url, handle) = super::spawn_delayed_model_catalog_server(None, 503, Duration::ZERO, None);
let error = fetch_models_dev_catalog_with_timeouts(
&url,
Duration::from_millis(100),
Duration::from_secs(1),
)
.unwrap_err();
handle.join().unwrap();
assert_eq!(error, ModelsDevFetchError::HttpStatus(503));
}
#[test]
fn models_dev_failure_categories_are_granular_and_sanitized() {
assert_eq!(
ModelsDevFetchError::Transport.category(),
CatalogRefreshFailureCategory::ModelsDevTransport
);
assert_eq!(
ModelsDevFetchError::Timeout.category(),
CatalogRefreshFailureCategory::ModelsDevTimeout
);
assert_eq!(
ModelsDevFetchError::HttpStatus(503).category(),
CatalogRefreshFailureCategory::ModelsDevHttpStatus
);
assert_eq!(
ModelsDevFetchError::BodyLimit.category(),
CatalogRefreshFailureCategory::ModelsDevBodyLimit
);
assert_eq!(
ModelsDevFetchError::JsonDecode.category(),
CatalogRefreshFailureCategory::ModelsDevJsonDecode
);
assert!(!ModelsDevFetchError::Transport.to_string().contains("http"));
}
#[test]
fn models_dev_lookup_fetches_at_most_once_per_instance() {
let calls = std::cell::Cell::new(0);
let mut lookup = ModelsDevLookup::default();
let first = lookup
.get_or_fetch(|| {
calls.set(calls.get() + 1);
Ok(serde_json::json!({"openai": {"models": {}}}))
})
.is_some();
let second = lookup
.get_or_fetch(|| panic!("models.dev fetch repeated after success"))
.is_some();
assert!(first);
assert!(second);
assert_eq!(calls.get(), 1);
let failed_calls = std::cell::Cell::new(0);
let mut failed_lookup = ModelsDevLookup::default();
assert!(
failed_lookup
.get_or_fetch(|| {
failed_calls.set(failed_calls.get() + 1);
anyhow::bail!("offline")
})
.is_none()
);
assert!(
failed_lookup
.get_or_fetch(|| {
failed_calls.set(failed_calls.get() + 1);
Ok(serde_json::json!({"openai": {"models": {}}}))
})
.is_some()
);
assert_eq!(failed_calls.get(), 2);
}
#[test]
fn custom_provider_catalog_enriches_from_explicit_models_dev_namespace() {
let fixture = serde_json::json!({
"openrouter": {
"models": {
"gpt-test": {
"description": "metadata description",
"limit": {"context": 128000_u64, "output": 4096_u64},
"reasoning": {"effort": true},
"modalities": ["text"]
}
}
}
});
let entry = ModelCatalogEntry::new("local-ai", "gpt-test");
let entries = enrich_with_models_dev_namespace(vec![entry], &fixture, "openrouter");
let enriched = &entries[0];
assert_eq!(enriched.provider, "local-ai");
assert_eq!(
enriched.description.as_deref(),
Some("metadata description")
);
assert_eq!(enriched.context_window, Some(128000));
assert_eq!(enriched.max_context_window, None);
assert_eq!(enriched.max_output_tokens, Some(4096));
assert_eq!(enriched.reasoning_efforts, None);
assert_eq!(enriched.supports_reasoning, Some(true));
assert_eq!(enriched.modalities, Some(vec!["text".to_string()]));
}
#[test]
fn custom_provider_openai_enriches_from_fixture_and_preserves_provider_id() {
let fixture = serde_json::json!({
"openai": {
"models": {
"gpt-5.5": {
"description": "fixture description",
"limit": {"context": 400000_u64, "output": 32768_u64},
"reasoning": {"effort": true},
"cost": {"input": 1, "output": 8},
"modalities": ["text", "image"]
}
}
}
});
let custom = CustomProviderConfig {
label: "OpenAI Compatible".to_string(),
base_url: "https://api.openai.com/v1".to_string(),
fast_mode: None,
api_key_env_var: Some("OPENAI_COMPATIBLE_API_KEY".to_string()),
models_dev_provider: Some("openai".to_string()),
use_responses_endpoint: false,
supports_text_verbosity: false,
reasoning_protocol: crate::config::CustomReasoningProtocol::default(),
extra_models: Vec::new(),
request_headers: Default::default(),
};
let entry = ModelCatalogEntry::new("openai", "gpt-5.5");
let entries = enrich_custom_provider_catalog_with_models_dev_value(
vec![entry],
"openai",
&custom,
&fixture,
);
let enriched = &entries[0];
assert_eq!(enriched.provider, "openai");
assert_eq!(enriched.id, "openai/gpt-5.5");
assert_eq!(enriched.description.as_deref(), Some("fixture description"));
assert_eq!(enriched.context_window, Some(400000));
assert_eq!(enriched.max_output_tokens, Some(32768));
assert_eq!(enriched.reasoning_efforts, None);
assert_eq!(enriched.supports_reasoning, Some(true));
assert_eq!(enriched.input_cost.as_deref(), Some("1"));
assert_eq!(enriched.output_cost.as_deref(), Some("8"));
assert_eq!(
enriched.modalities,
Some(vec!["text".to_string(), "image".to_string()])
);
}
#[test]
fn custom_provider_openai_current_fixture_covers_all_gpt56_models() {
let models = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
let fixture = serde_json::json!({
"openai": {
"models": {
"gpt-5.6-sol": {"reasoning": true, "reasoning_options": [{"type": "effort", "values": ["none", "low", "medium", "high", "xhigh", "max"]}]},
"gpt-5.6-terra": {"reasoning": true, "reasoning_options": [{"type": "effort", "values": ["none", "low", "medium", "high", "xhigh", "max"]}]},
"gpt-5.6-luna": {"reasoning": true, "reasoning_options": [{"type": "effort", "values": ["none", "low", "medium", "high", "xhigh", "max"]}]}
}
}
});
let entries = enrich_with_models_dev_namespace(
models
.iter()
.map(|model| ModelCatalogEntry::new("openai", *model))
.collect(),
&fixture,
"openai",
);
for entry in entries {
assert_eq!(entry.supports_reasoning, Some(true));
assert_eq!(
entry.reasoning_efforts,
Some(vec![
ThinkingLevel::Default,
ThinkingLevel::Low,
ThinkingLevel::Medium,
ThinkingLevel::High,
ThinkingLevel::XHigh,
ThinkingLevel::Max,
])
);
}
}
#[test]
fn models_dev_enrichment_preserves_provider_reasoning_metadata() {
let provider_levels = ThinkingLevel::ALL.to_vec();
let mut entry = ModelCatalogEntry::new_codex("gpt-6-astra");
entry.reasoning_efforts = Some(provider_levels.clone());
entry.supports_reasoning = Some(true);
let fixture = serde_json::json!({
"openai": {
"models": {
"gpt-6-astra": {
"reasoning": true,
"reasoning_options": [{"type": "effort", "values": ["low", "high"]}]
}
}
}
});
let entries = enrich_with_models_dev_namespace(vec![entry], &fixture, "openai");
assert_eq!(entries[0].reasoning_efforts, Some(provider_levels));
assert_eq!(entries[0].supports_reasoning, Some(true));
}
#[test]
fn custom_provider_missing_models_dev_provider_falls_back_to_provider_id() {
let fixture = serde_json::json!({
"hosted-provider": {
"models": {
"gpt-5.5": {
"description": "fixture description",
"limit": {"context": 400000_u64, "output": 32768_u64},
"reasoning": {"effort": true},
"modalities": ["text"]
}
}
}
});
let custom = CustomProviderConfig {
label: "Hosted Provider".to_string(),
base_url: "https://provider.example/v1".to_string(),
fast_mode: None,
api_key_env_var: Some("HOSTED_PROVIDER_API_KEY".to_string()),
models_dev_provider: None,
use_responses_endpoint: false,
supports_text_verbosity: false,
reasoning_protocol: crate::config::CustomReasoningProtocol::default(),
extra_models: Vec::new(),
request_headers: Default::default(),
};
let entry = ModelCatalogEntry::new("hosted-provider", "gpt-5.5");
let entries = enrich_custom_provider_catalog_with_models_dev_value(
vec![entry],
"hosted-provider",
&custom,
&fixture,
);
let enriched = &entries[0];
assert_eq!(enriched.provider, "hosted-provider");
assert_eq!(enriched.id, "hosted-provider/gpt-5.5");
assert_eq!(enriched.description.as_deref(), Some("fixture description"));
assert_eq!(enriched.context_window, Some(400000));
assert_eq!(enriched.max_output_tokens, Some(32768));
assert_eq!(enriched.reasoning_efforts, None);
assert_eq!(enriched.supports_reasoning, Some(true));
assert_eq!(enriched.modalities, Some(vec!["text".to_string()]));
}
#[test]
fn custom_provider_invalid_base_url_falls_back_to_provider_id_namespace() {
let fixture = serde_json::json!({
"invalid-provider": {
"models": {
"gpt-5.5": {
"description": "fixture description",
"limit": {"context": 400000_u64}
}
}
}
});
let custom = CustomProviderConfig {
label: "Invalid Provider".to_string(),
base_url: "not a url".to_string(),
fast_mode: None,
api_key_env_var: None,
models_dev_provider: None,
use_responses_endpoint: false,
supports_text_verbosity: false,
reasoning_protocol: crate::config::CustomReasoningProtocol::default(),
extra_models: Vec::new(),
request_headers: Default::default(),
};
let entry = ModelCatalogEntry::new("invalid-provider", "gpt-5.5");
let entries = enrich_custom_provider_catalog_with_models_dev_value(
vec![entry],
"invalid-provider",
&custom,
&fixture,
);
let enriched = &entries[0];
assert_eq!(enriched.provider, "invalid-provider");
assert_eq!(enriched.id, "invalid-provider/gpt-5.5");
assert_eq!(enriched.description.as_deref(), Some("fixture description"));
assert_eq!(enriched.context_window, Some(400000));
}
#[test]
fn custom_provider_openai_base_does_not_override_provider_id_fallback() {
let fixture = serde_json::json!({
"openai": {
"models": {
"gpt-5.5": {
"description": "openai description",
"limit": {"context": 400000_u64}
}
}
},
"custom-openai": {
"models": {
"gpt-5.5": {
"description": "custom namespace description",
"limit": {"context": 123000_u64, "output": 4096_u64}
}
}
}
});
let custom = CustomProviderConfig {
label: "OpenAI Compatible".to_string(),
base_url: "https://api.openai.com/v1".to_string(),
fast_mode: None,
api_key_env_var: Some("OPENAI_COMPATIBLE_API_KEY".to_string()),
models_dev_provider: None,
use_responses_endpoint: false,
supports_text_verbosity: false,
reasoning_protocol: crate::config::CustomReasoningProtocol::default(),
extra_models: Vec::new(),
request_headers: Default::default(),
};
let entry = ModelCatalogEntry::new("custom-openai", "gpt-5.5");
let entries = enrich_custom_provider_catalog_with_models_dev_value(
vec![entry],
"custom-openai",
&custom,
&fixture,
);
let enriched = &entries[0];
assert_eq!(enriched.provider, "custom-openai");
assert_eq!(enriched.id, "custom-openai/gpt-5.5");
assert_eq!(
enriched.description.as_deref(),
Some("custom namespace description")
);
assert_eq!(enriched.context_window, Some(123000));
assert_eq!(enriched.max_output_tokens, Some(4096));
}
#[test]
fn custom_provider_id_openai_falls_back_to_openai_namespace() {
let fixture = serde_json::json!({
"openai": {
"models": {
"gpt-5.5": {
"description": "fixture description",
"limit": {"context": 400000_u64, "output": 32768_u64},
"reasoning": {"effort": true},
"modalities": ["text", "image"]
}
}
}
});
let custom = CustomProviderConfig {
label: "OpenAI Compatible".to_string(),
base_url: "https://api.openai.com/v1".to_string(),
fast_mode: None,
api_key_env_var: Some("OPENAI_COMPATIBLE_API_KEY".to_string()),
models_dev_provider: None,
use_responses_endpoint: false,
supports_text_verbosity: false,
reasoning_protocol: crate::config::CustomReasoningProtocol::default(),
extra_models: Vec::new(),
request_headers: Default::default(),
};
let entry = ModelCatalogEntry::new("openai", "gpt-5.5");
let entries = enrich_custom_provider_catalog_with_models_dev_value(
vec![entry],
"openai",
&custom,
&fixture,
);
let enriched = &entries[0];
assert_eq!(enriched.provider, "openai");
assert_eq!(enriched.id, "openai/gpt-5.5");
assert_eq!(enriched.description.as_deref(), Some("fixture description"));
assert_eq!(enriched.context_window, Some(400000));
assert_eq!(enriched.max_output_tokens, Some(32768));
assert_eq!(enriched.reasoning_efforts, None);
assert_eq!(enriched.supports_reasoning, Some(true));
assert_eq!(
enriched.modalities,
Some(vec!["text".to_string(), "image".to_string()])
);
}
#[test]
fn custom_provider_explicit_models_dev_provider_overrides_provider_id_fallback() {
let fixture = serde_json::json!({
"hosted-provider": {
"models": {
"gpt-5.5": {
"description": "provider-id description",
"limit": {"context": 400000_u64}
}
}
},
"openrouter": {
"models": {
"gpt-5.5": {
"description": "openrouter description",
"limit": {"context": 128000_u64}
}
}
}
});
let custom = CustomProviderConfig {
label: "Hosted Provider".to_string(),
base_url: "https://api.openai.com/v1".to_string(),
fast_mode: None,
api_key_env_var: Some("HOSTED_PROVIDER_API_KEY".to_string()),
models_dev_provider: Some("openrouter".to_string()),
use_responses_endpoint: false,
supports_text_verbosity: false,
reasoning_protocol: crate::config::CustomReasoningProtocol::default(),
extra_models: Vec::new(),
request_headers: Default::default(),
};
let entry = ModelCatalogEntry::new("hosted-provider", "gpt-5.5");
let entries = enrich_custom_provider_catalog_with_models_dev_value(
vec![entry],
"hosted-provider",
&custom,
&fixture,
);
let enriched = &entries[0];
assert_eq!(enriched.provider, "hosted-provider");
assert_eq!(enriched.id, "hosted-provider/gpt-5.5");
assert_eq!(
enriched.description.as_deref(),
Some("openrouter description")
);
assert_eq!(enriched.context_window, Some(128000));
}
#[test]
fn custom_provider_catalog_includes_extra_models_and_dedupes_live_models() {
let temp = TempDir::new().unwrap();
let paths = super::super::McPaths::from_root(temp.path().join("mc"));
let (base_url, handle) = super::spawn_model_catalog_server("glm-5.1");
let custom = CustomProviderConfig {
label: "Z.ai".to_string(),
base_url,
fast_mode: None,
api_key_env_var: None,
models_dev_provider: None,
use_responses_endpoint: false,
supports_text_verbosity: false,
reasoning_protocol: crate::config::CustomReasoningProtocol::default(),
extra_models: vec!["glm-5.2".to_string(), "glm-5.1".to_string()],
request_headers: Default::default(),
};
let models_dev = SharedModelsDevLookup::from_seeded(&ModelsDevLookup {
attempted: true,
value: Some(serde_json::json!({})),
});
let result = load_custom_provider_catalog_with_models_dev(
&paths,
"zai",
&custom,
Some(&crate::config::ProviderCredential::NoAuth),
CachePreference::AllowStale,
Some(&models_dev),
)
.unwrap();
handle.join().unwrap();
let ids = result
.entries
.iter()
.map(|entry| entry.id.as_str())
.collect::<Vec<_>>();
assert_eq!(ids, vec!["zai/glm-5.1", "zai/glm-5.2"]);
assert_eq!(
result
.entries
.iter()
.filter(|entry| entry.model == "glm-5.1")
.count(),
1
);
}
#[test]
fn custom_provider_catalog_skips_unknown_models_dev_namespace() {
let fixture = serde_json::json!({
"openrouter": {"models": {"gpt-test": {"limit": {"context": 128000_u64}}}}
});
let custom = CustomProviderConfig {
label: "Missing Provider".to_string(),
base_url: "https://provider.example/v1".to_string(),
fast_mode: None,
api_key_env_var: None,
models_dev_provider: None,
use_responses_endpoint: false,
supports_text_verbosity: false,
reasoning_protocol: crate::config::CustomReasoningProtocol::default(),
extra_models: Vec::new(),
request_headers: Default::default(),
};
let entry = ModelCatalogEntry::new("missing-provider", "gpt-test");
let entries = enrich_custom_provider_catalog_with_models_dev_value(
vec![entry.clone()],
"missing-provider",
&custom,
&fixture,
);
assert_eq!(entries, vec![entry]);
}
#[test]
fn models_dev_output_limit_does_not_become_context_window() {
let fixture = serde_json::json!({
"openrouter": {"models": {"gpt-test": {"limit": {"output": 4096_u64}}}}
});
let entry = ModelCatalogEntry::new("local-ai", "gpt-test");
let entries = enrich_with_models_dev_namespace(vec![entry], &fixture, "openrouter");
let enriched = &entries[0];
assert_eq!(enriched.context_window, None);
assert_eq!(enriched.max_context_window, None);
assert_eq!(enriched.max_output_tokens, Some(4096));
}
#[test]
fn models_dev_refresh_clears_removed_reasoning_capability() {
let mut entry = ModelCatalogEntry::new("local-ai", "gpt-test");
entry.supports_reasoning = Some(true);
entry.reasoning_efforts = Some(ThinkingLevel::EFFORT_GENERIC.to_vec());
let entries = enrich_with_models_dev_namespace(
vec![entry],
&serde_json::json!({"openai": {"models": {"gpt-test": {"reasoning": false}}}}),
"openai",
);
assert_eq!(entries[0].supports_reasoning, Some(false));
assert_eq!(entries[0].reasoning_efforts, None);
}
#[test]
fn models_dev_source_reports_partial_match_diagnostics() {
let lookup = SharedModelsDevLookup::from_seeded(&ModelsDevLookup {
attempted: true,
value: Some(serde_json::json!({"openai": {"models": {"gpt-test": {}}}})),
});
let source = models_dev_source(
&[
ModelCatalogEntry::new("local-ai", "gpt-test"),
ModelCatalogEntry::new("local-ai", "missing"),
],
Some(&lookup),
"openai",
);
assert!(matches!(source, CatalogSource::LivePartialMatch { .. }));
assert!(source.notice().unwrap().contains("missing"));
}