use super::*;
use assert_fs::TempDir;
use std::fs;
fn find_workspace_root() -> PathBuf {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut current = manifest.as_path();
while let Some(parent) = current.parent() {
let cargo_toml = parent.join("Cargo.toml");
if cargo_toml.is_file() && fs::read_to_string(&cargo_toml).is_ok_and(|c| c.contains("[workspace]")) {
return parent.to_path_buf();
}
current = parent;
}
panic!("could not find workspace root from CARGO_MANIFEST_DIR");
}
#[test]
fn prompt_caching_defaults_align_with_constants() {
let cfg = PromptCachingConfig::default();
assert!(cfg.enabled);
assert_eq!(cfg.max_entries, prompt_cache::DEFAULT_MAX_ENTRIES);
assert_eq!(cfg.max_age_days, prompt_cache::DEFAULT_MAX_AGE_DAYS);
assert!((cfg.min_quality_threshold - prompt_cache::DEFAULT_MIN_QUALITY_THRESHOLD).abs() < f64::EPSILON);
assert_eq!(cfg.cache_friendly_prompt_shaping, prompt_cache::DEFAULT_CACHE_FRIENDLY_PROMPT_SHAPING);
assert!(cfg.providers.openai.enabled);
assert_eq!(cfg.providers.openai.min_prefix_tokens, prompt_cache::OPENAI_MIN_PREFIX_TOKENS);
assert_eq!(cfg.providers.openai.prompt_cache_key_mode, OpenAIPromptCacheKeyMode::Session);
assert_eq!(cfg.providers.anthropic.extended_ttl_seconds, Some(prompt_cache::ANTHROPIC_EXTENDED_TTL_SECONDS));
assert_eq!(cfg.providers.gemini.mode, GeminiPromptCacheMode::Implicit);
assert!(cfg.providers.moonshot.enabled);
assert_eq!(cfg.providers.openai.prompt_cache_retention, None);
}
#[test]
fn resolve_cache_dir_expands_home() {
let cfg = PromptCachingConfig {
cache_dir: "~/.custom/cache".to_string(),
..PromptCachingConfig::default()
};
let resolved = cfg.resolve_cache_dir(None);
if let Some(home) = dirs::home_dir() {
assert!(resolved.starts_with(home));
} else {
assert_eq!(resolved, PathBuf::from(".custom/cache"));
}
}
#[test]
fn resolve_cache_dir_uses_workspace_when_relative() {
let temp = TempDir::new().unwrap();
let workspace = temp.path();
let cfg = PromptCachingConfig {
cache_dir: "relative/cache".to_string(),
..PromptCachingConfig::default()
};
let resolved = cfg.resolve_cache_dir(Some(workspace));
assert_eq!(resolved, workspace.join("relative/cache"));
}
#[test]
fn prompt_cache_retention_deserializes_valid_values() {
let cfg: OpenAIPromptCacheSettings = toml::from_str(
r#"
prompt_cache_retention = "in_memory"
"#,
)
.unwrap();
assert_eq!(cfg.prompt_cache_retention, Some(PromptCacheRetention::InMemory));
let cfg2: OpenAIPromptCacheSettings = toml::from_str(
r#"
prompt_cache_retention = "24h"
"#,
)
.unwrap();
assert_eq!(cfg2.prompt_cache_retention, Some(PromptCacheRetention::H24));
}
#[test]
fn prompt_cache_retention_unknown_values_deserialize_as_unknown() {
let cfg: OpenAIPromptCacheSettings = toml::from_str(
r#"
prompt_cache_retention = "5m"
"#,
)
.unwrap();
assert_eq!(cfg.prompt_cache_retention, Some(PromptCacheRetention::Unknown));
}
#[test]
fn prompt_cache_retention_as_str_matches_serde() {
for retention in [
PromptCacheRetention::InMemory,
PromptCacheRetention::H24,
PromptCacheRetention::Unknown,
] {
assert_eq!(
serde_json::to_value(retention).unwrap(),
serde_json::json!(retention.as_str()),
"serde wire form drifted from as_str() for {retention:?}"
);
}
}
#[test]
fn validate_prompt_cache_rejects_unknown_retention() {
let mut cfg = PromptCachingConfig::default();
cfg.providers.openai.prompt_cache_retention = Some(PromptCacheRetention::Unknown);
assert!(cfg.validate().is_err());
}
#[test]
fn prompt_cache_key_mode_parses_from_toml() {
let parsed: PromptCachingConfig = toml::from_str(
r#"
[providers.openai]
prompt_cache_key_mode = "off"
"#,
)
.expect("prompt cache config should parse");
assert_eq!(parsed.providers.openai.prompt_cache_key_mode, OpenAIPromptCacheKeyMode::Off);
}
#[test]
fn build_openai_prompt_cache_key_uses_trimmed_lineage_id() {
let key = build_openai_prompt_cache_key(true, &OpenAIPromptCacheKeyMode::Session, Some(" lineage-abc "));
assert_eq!(key.as_deref(), Some("vtcode:openai:lineage-abc"));
}
#[test]
fn map_prompt_cache_key_namespaces_providers() {
assert_eq!(
map_prompt_cache_key_for_provider("merge-gateway", "vtcode:openai:abc".to_string()),
"vtcode:merge:abc"
);
assert_eq!(map_prompt_cache_key_for_provider("openai", "vtcode:openai:abc".to_string()), "vtcode:openai:abc");
assert_eq!(
map_prompt_cache_key_for_provider("openrouter", "vtcode:openai:abc".to_string()),
"vtcode:openrouter:abc"
);
assert_eq!(map_prompt_cache_key_for_provider("xai", "vtcode:openai:abc".to_string()), "vtcode:xai:abc");
assert_eq!(
map_prompt_cache_key_for_provider("opencode-go", "vtcode:openai:abc".to_string()),
"vtcode:opencode-go:abc"
);
}
#[test]
fn session_affinity_key_gate_covers_sticky_routing_providers() {
assert!(session_affinity_key_enabled("openrouter", true, false));
assert!(session_affinity_key_enabled("xai", true, false));
assert!(session_affinity_key_enabled("opencode-go", true, false));
assert!(session_affinity_key_enabled("openai", true, true));
assert!(!session_affinity_key_enabled("openai", true, false));
assert!(!session_affinity_key_enabled("openrouter", false, true));
assert!(!session_affinity_key_enabled("opencode-go", false, true));
assert!(!session_affinity_key_enabled("anthropic", true, true));
assert!(session_affinity_provider("openrouter"));
assert!(session_affinity_provider("xai"));
assert!(session_affinity_provider("opencode-go"));
assert!(!session_affinity_provider("anthropic"));
}
#[test]
fn build_session_affinity_prompt_cache_key_namespaces_providers() {
let mode = OpenAIPromptCacheKeyMode::Session;
assert_eq!(
build_session_affinity_prompt_cache_key("openai", true, &mode, Some("lineage-1")).as_deref(),
Some("vtcode:openai:lineage-1")
);
assert_eq!(
build_session_affinity_prompt_cache_key("merge-gateway", true, &mode, Some("lineage-1")).as_deref(),
Some("vtcode:merge:lineage-1")
);
assert_eq!(
build_session_affinity_prompt_cache_key("openrouter", true, &mode, Some("lineage-1")).as_deref(),
Some("vtcode:openrouter:lineage-1")
);
assert_eq!(
build_session_affinity_prompt_cache_key("xai", true, &mode, Some("lineage-1")).as_deref(),
Some("vtcode:xai:lineage-1")
);
assert_eq!(
build_session_affinity_prompt_cache_key("opencode-go", true, &mode, Some("lineage-1")).as_deref(),
Some("vtcode:opencode-go:lineage-1")
);
assert_eq!(
build_session_affinity_prompt_cache_key("openai", true, &OpenAIPromptCacheKeyMode::Off, Some("lineage-1")),
None
);
assert_eq!(
build_session_affinity_prompt_cache_key("openrouter", true, &OpenAIPromptCacheKeyMode::Off, Some("lineage-1"))
.as_deref(),
Some("vtcode:openrouter:lineage-1")
);
assert_eq!(
build_session_affinity_prompt_cache_key("opencode-go", true, &OpenAIPromptCacheKeyMode::Off, Some("lineage-1"))
.as_deref(),
Some("vtcode:opencode-go:lineage-1")
);
assert_eq!(build_session_affinity_prompt_cache_key("openrouter", false, &mode, Some("lineage-1")), None);
assert_eq!(build_session_affinity_prompt_cache_key("openrouter", true, &mode, Some(" ")), None);
assert_eq!(build_session_affinity_prompt_cache_key("anthropic", true, &mode, Some("lineage-1")), None);
}
#[test]
fn build_openai_prompt_cache_key_honors_disabled_or_off_mode() {
assert_eq!(build_openai_prompt_cache_key(false, &OpenAIPromptCacheKeyMode::Session, Some("id")), None);
assert_eq!(build_openai_prompt_cache_key(true, &OpenAIPromptCacheKeyMode::Off, Some("id")), None);
assert_eq!(build_openai_prompt_cache_key(true, &OpenAIPromptCacheKeyMode::Session, Some(" ")), None);
}
#[test]
fn provider_enablement_respects_global_and_provider_flags() {
let mut cfg = PromptCachingConfig { enabled: true, ..PromptCachingConfig::default() };
cfg.providers.openai.enabled = true;
assert!(cfg.is_provider_enabled("openai"));
assert!(cfg.is_provider_enabled("merge-gateway"));
cfg.enabled = false;
assert!(!cfg.is_provider_enabled("openai"));
assert!(!cfg.is_provider_enabled("merge-gateway"));
}
#[test]
fn provider_enablement_handles_aliases_and_modes() {
let mut cfg = PromptCachingConfig { enabled: true, ..PromptCachingConfig::default() };
cfg.providers.anthropic.enabled = true;
assert!(cfg.is_provider_enabled("minimax"));
cfg.providers.gemini.enabled = true;
cfg.providers.gemini.mode = GeminiPromptCacheMode::Off;
assert!(!cfg.is_provider_enabled("gemini"));
}
#[test]
fn gap_threshold_uses_provider_defaults() {
let cfg = PromptCachingConfig::default();
assert_eq!(cfg.gap_threshold_secs("anthropic"), Some(prompt_cache::ANTHROPIC_CACHE_GAP_WARNING_SECONDS));
assert_eq!(cfg.gap_threshold_secs("minimax"), Some(prompt_cache::ANTHROPIC_CACHE_GAP_WARNING_SECONDS));
assert_eq!(cfg.gap_threshold_secs("openai"), Some(prompt_cache::OPENAI_CACHE_GAP_WARNING_SECONDS));
assert_eq!(cfg.gap_threshold_secs("gemini"), Some(prompt_cache::DEFAULT_CACHE_GAP_WARNING_SECONDS));
}
#[test]
fn gap_threshold_uses_model_ttl_and_ignores_legacy_retention_for_modern_models() {
let mut cfg = PromptCachingConfig::default();
for retention in [
None,
Some(PromptCacheRetention::H24),
Some(PromptCacheRetention::InMemory),
] {
cfg.providers.openai.prompt_cache_retention = retention;
for model in ["gpt-5.6-sol", "gpt-6-luna", "gpt-6.1-sol"] {
assert_eq!(cfg.gap_threshold_secs_for_model("openai", model), Some(1800), "{model}");
}
}
assert_eq!(cfg.gap_threshold_secs_for_model("openai", "gpt-5.1-mini"), Some(600));
assert_eq!(cfg.gap_threshold_secs_for_model("merge-gateway", "gpt-6-luna"), Some(300));
cfg.gap_warning_threshold_secs = Some(77);
assert_eq!(cfg.gap_threshold_secs_for_model("openai", "gpt-6-luna"), Some(77));
cfg.enabled = false;
assert_eq!(cfg.gap_threshold_secs_for_model("openai", "gpt-6-luna"), None);
}
#[test]
fn explicit_cache_models_reject_migration_aliases_and_auxiliary_mode_is_opt_in() {
for model in [
"gpt-5.4",
"gpt-5-codex",
"gpt",
"gpt-5.1-mini",
"openai/gpt-6-luna",
"gpt-6-future",
] {
assert!(!supports_openai_explicit_prompt_cache(model), "{model}");
}
for model in ["gpt-5.6", "gpt-5.6-terra", "gpt-6-astra", "gpt-6-sol"] {
assert!(supports_openai_explicit_prompt_cache(model), "{model}");
}
let defaults: OpenAIPromptCacheSettings = toml::from_str("").unwrap();
assert!(!defaults.auxiliary_explicit_cache);
let configured: OpenAIPromptCacheSettings = toml::from_str("auxiliary_explicit_cache = true").unwrap();
assert!(configured.auxiliary_explicit_cache);
}
#[test]
fn gap_threshold_respects_disable_switches() {
let cfg = PromptCachingConfig { gap_warning_enabled: false, ..Default::default() };
assert_eq!(cfg.gap_threshold_secs("anthropic"), None);
let mut cfg = PromptCachingConfig::default();
cfg.providers.anthropic.enabled = false;
assert_eq!(cfg.gap_threshold_secs("anthropic"), None);
assert_eq!(cfg.gap_threshold_secs("minimax"), None);
let cfg = PromptCachingConfig { enabled: false, ..Default::default() };
assert_eq!(cfg.gap_threshold_secs("openai"), None);
}
#[test]
fn gap_threshold_treats_extended_retention_as_an_advisory_not_a_guarantee() {
let mut cfg = PromptCachingConfig::default();
cfg.providers.openai.prompt_cache_retention = Some(PromptCacheRetention::H24);
assert_eq!(cfg.gap_threshold_secs("openai"), Some(1800));
cfg.gap_warning_threshold_secs = Some(42);
assert_eq!(cfg.gap_threshold_secs("openai"), Some(42));
cfg.gap_warning_threshold_secs = None;
cfg.providers.openai.prompt_cache_retention = Some(PromptCacheRetention::InMemory);
assert_eq!(cfg.gap_threshold_secs("openai"), Some(prompt_cache::OPENAI_CACHE_GAP_WARNING_SECONDS));
}
#[test]
fn gap_threshold_honors_explicit_override() {
let cfg = PromptCachingConfig {
gap_warning_threshold_secs: Some(42),
..Default::default()
};
assert_eq!(cfg.gap_threshold_secs("anthropic"), Some(42));
assert_eq!(cfg.gap_threshold_secs("openai"), Some(42));
}
#[test]
fn gap_warning_fields_parse_from_toml() {
let parsed: PromptCachingConfig = toml::from_str(
r#"
gap_warning_enabled = false
gap_warning_threshold_secs = 120
"#,
)
.expect("prompt cache config should parse");
assert!(!parsed.gap_warning_enabled);
assert_eq!(parsed.gap_warning_threshold_secs, Some(120));
let defaults: PromptCachingConfig = toml::from_str("").expect("empty config");
assert!(defaults.gap_warning_enabled);
assert_eq!(defaults.gap_warning_threshold_secs, None);
}
#[test]
fn bundled_config_templates_match_prompt_cache_defaults() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let bootstrap_template =
fs::read_to_string(manifest_dir.join("data/default_config.toml")).expect("bootstrap config template");
let config: crate::VTCodeConfig = toml::from_str(&bootstrap_template).expect("parse bootstrap template");
assert!(config.prompt_cache.enabled);
assert!(config.prompt_cache.cache_friendly_prompt_shaping);
assert!(bootstrap_template.contains("# prompt_cache_retention = \"24h\""));
let workspace_root = find_workspace_root();
let example_config = fs::read_to_string(workspace_root.join("vtcode.toml.example")).expect("vtcode.toml.example");
if example_config.contains("[prompt_cache]") {
assert!(example_config.contains("enabled = true"));
assert!(example_config.contains("cache_friendly_prompt_shaping = true"));
assert!(example_config.contains("# prompt_cache_retention = \"24h\""));
}
let prompt_cache_guide =
fs::read_to_string(workspace_root.join("docs/tools/PROMPT_CACHING_GUIDE.md")).expect("prompt caching guide");
let normalized_guide = vtcode_commons::formatting::collapse_whitespace(&prompt_cache_guide);
assert!(
normalized_guide.contains("VT Code enables `prompt_cache.cache_friendly_prompt_shaping = true` by default.")
);
assert!(normalized_guide.contains("`prompt_cache_retention` is opt-in for older supported Responses models"));
let field_reference = fs::read_to_string(workspace_root.join("docs/config/CONFIG_FIELD_REFERENCE.md"))
.expect("config field reference");
assert!(field_reference.contains("`prompt_cache.cache_friendly_prompt_shaping`"));
assert!(field_reference.contains("`prompt_cache.providers.openai.prompt_cache_retention`"));
}
#[test]
fn bundled_example_config_parses_and_validates() {
let workspace_root = find_workspace_root();
let example = fs::read_to_string(workspace_root.join("vtcode.toml.example")).expect("vtcode.toml.example");
let config: crate::VTCodeConfig = toml::from_str(&example).expect("example config should parse");
config.validate().expect("example config should validate");
}