Skip to content

Commit 63c056d

Browse files
test(response-cache): cover cache failure paths
Signed-off-by: Zhongxuan Wang <daniewang@nvidia.com>
1 parent c8f776a commit 63c056d

16 files changed

Lines changed: 1419 additions & 10 deletions

codecov.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ component_management:
4949
- "crates/adaptive/src"
5050
statuses:
5151
- type: project
52-
target: auto
52+
target: 95%
5353
threshold: 0.5%
5454
base: auto
5555
if_ci_failed: error

crates/adaptive/tests/integration/response_cache_tests.rs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ use nemo_relay::api::subscriber::{deregister_subscriber, flush_subscribers, regi
2525
use nemo_relay::api::tool::{ToolCallExecuteParams, tool_call_execute};
2626
use nemo_relay::error::FlowError;
2727
use nemo_relay::plugin::{
28-
PluginConfig, clear_plugin_configuration, initialize_plugins_exact, validate_plugin_config,
28+
DiagnosticLevel, PluginConfig, clear_plugin_configuration, initialize_plugins_exact,
29+
validate_plugin_config,
2930
};
3031
use nemo_relay_adaptive::plugin_component::{ComponentSpec, register_adaptive_component};
3132
use nemo_relay_adaptive::{
@@ -551,6 +552,7 @@ async fn invalid_config_is_rejected_by_validation() {
551552
response_cache: Some(ResponseCacheConfig {
552553
ttl_seconds: 0,
553554
bypass_rate: 2.0,
555+
key_strategy: "semantic".to_string(),
554556
namespace: "invalid-config-test".to_string(),
555557
..ResponseCacheConfig::default()
556558
}),
@@ -575,6 +577,13 @@ async fn invalid_config_is_rejected_by_validation() {
575577
.any(|diagnostic| diagnostic.code == "response_cache.invalid_bypass_rate"),
576578
"bypass_rate out of range must produce a diagnostic"
577579
);
580+
assert!(
581+
report
582+
.diagnostics
583+
.iter()
584+
.any(|diagnostic| diagnostic.code == "response_cache.unsupported_key_strategy"),
585+
"an unsupported key strategy must produce a diagnostic"
586+
);
578587
}
579588

580589
#[tokio::test]
@@ -627,6 +636,81 @@ async fn unknown_and_unavailable_backends_are_rejected_by_validation() {
627636
}
628637
}
629638

639+
#[tokio::test]
640+
async fn response_cache_validation_diagnostics_identify_the_invalid_setting() {
641+
let _guard = TEST_MUTEX.lock().await;
642+
reset_global();
643+
register_adaptive_component().unwrap();
644+
645+
let mut cache = ResponseCacheConfig {
646+
namespace: "diagnostic-contract-test".to_string(),
647+
key_strategy: "semantic".to_string(),
648+
tools: Some(ToolCacheConfig {
649+
enabled: true,
650+
default: ToolClass {
651+
bypass_rate: Some(-0.01),
652+
..ToolClass::default()
653+
},
654+
..ToolCacheConfig::default()
655+
}),
656+
..ResponseCacheConfig::default()
657+
};
658+
cache.backend.kind = "redis".to_string();
659+
660+
let report = validate_plugin_config(&PluginConfig {
661+
components: vec![
662+
ComponentSpec::new(AdaptiveConfig {
663+
response_cache: Some(cache),
664+
..AdaptiveConfig::default()
665+
})
666+
.into(),
667+
],
668+
..PluginConfig::default()
669+
});
670+
671+
assert!(
672+
report.diagnostics.iter().any(|diagnostic| {
673+
diagnostic.code == "response_cache.unsupported_key_strategy"
674+
&& diagnostic.level == DiagnosticLevel::Error
675+
&& diagnostic.component.as_deref() == Some("response_cache")
676+
&& diagnostic.field.as_deref() == Some("key_strategy")
677+
}),
678+
"an unsupported key strategy must identify its setting: {:?}",
679+
report.diagnostics
680+
);
681+
assert!(
682+
report.diagnostics.iter().any(|diagnostic| {
683+
diagnostic.code == "response_cache.tool_invalid_bypass_rate"
684+
&& diagnostic.level == DiagnosticLevel::Error
685+
&& diagnostic.field.as_deref() == Some("tools")
686+
}),
687+
"an invalid tool bypass rate must identify the tools section: {:?}",
688+
report.diagnostics
689+
);
690+
691+
#[cfg(not(feature = "redis-backend"))]
692+
assert!(
693+
report.diagnostics.iter().any(|diagnostic| {
694+
diagnostic.code == "response_cache.backend_unavailable"
695+
&& diagnostic.level == DiagnosticLevel::Error
696+
&& diagnostic.field.as_deref() == Some("backend.kind")
697+
}),
698+
"redis must be rejected when its backend feature is not compiled: {:?}",
699+
report.diagnostics
700+
);
701+
702+
#[cfg(feature = "redis-backend")]
703+
assert!(
704+
report.diagnostics.iter().any(|diagnostic| {
705+
diagnostic.code == "response_cache.missing_redis_url"
706+
&& diagnostic.level == DiagnosticLevel::Error
707+
&& diagnostic.field.as_deref() == Some("backend.config.url")
708+
}),
709+
"redis must identify a missing connection URL when its backend feature is compiled: {:?}",
710+
report.diagnostics
711+
);
712+
}
713+
630714
#[tokio::test]
631715
async fn hit_preserves_usage_on_the_end_event_and_reports_savings_on_the_mark() {
632716
let _guard = TEST_MUTEX.lock().await;
@@ -2205,10 +2289,19 @@ async fn invalid_tool_config_is_rejected_by_validation() {
22052289
ToolClass {
22062290
cacheable: true,
22072291
ttl_seconds: Some(0),
2292+
bypass_rate: Some(1.1),
22082293
members: vec!["dup".to_string()],
22092294
..ToolClass::default()
22102295
},
22112296
);
2297+
let mut overrides = std::collections::BTreeMap::new();
2298+
overrides.insert(
2299+
"docs_lookup".to_string(),
2300+
ToolOverride {
2301+
bypass_rate: Some(-0.1),
2302+
..ToolOverride::default()
2303+
},
2304+
);
22122305
let adaptive = AdaptiveConfig {
22132306
response_cache: Some(cache_with_tools(ToolCacheConfig {
22142307
enabled: true,
@@ -2217,6 +2310,7 @@ async fn invalid_tool_config_is_rejected_by_validation() {
22172310
..ToolClass::default()
22182311
},
22192312
classes,
2313+
overrides,
22202314
..ToolCacheConfig::default()
22212315
})),
22222316
..AdaptiveConfig::default()
@@ -2242,6 +2336,14 @@ async fn invalid_tool_config_is_rejected_by_validation() {
22422336
"a zero class TTL must be rejected: {:?}",
22432337
report.diagnostics
22442338
);
2339+
assert!(
2340+
report
2341+
.diagnostics
2342+
.iter()
2343+
.any(|diagnostic| diagnostic.code == "response_cache.tool_invalid_bypass_rate"),
2344+
"out-of-range class and override bypass rates must be rejected: {:?}",
2345+
report.diagnostics
2346+
);
22452347
assert!(
22462348
report
22472349
.diagnostics

crates/adaptive/tests/unit/cache_diagnostics_tests.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,34 @@ fn cache_request_facts_keeps_missing_facts_bounded_when_inputs_are_unavailable()
232232
assert_eq!(facts.stable_prefix_tokens, None);
233233
}
234234

235+
#[test]
236+
fn cache_request_facts_rejects_a_truncated_stable_prefix() {
237+
let hot_cache = make_hot_cache(Some(2));
238+
let mut tracker = CacheDiagnosticsTracker::default();
239+
let prompt_ir = make_prompt_ir(vec![("system-0", "You are a careful planner", Some(700))]);
240+
241+
let facts = build_cache_request_facts_from_prompt_ir(
242+
CacheFactsBuildInput {
243+
agent_id: "agent-1",
244+
provider: "openai",
245+
model: Some("gpt-4o"),
246+
prompt_ir: &prompt_ir,
247+
hot_cache: &hot_cache,
248+
profile_key: "test-profile",
249+
now: sample_timestamp(),
250+
},
251+
&mut tracker,
252+
);
253+
254+
assert_eq!(facts.stable_prefix_length, 2);
255+
assert_eq!(facts.stable_prefix_tokens, None);
256+
assert!(
257+
facts
258+
.missing_facts
259+
.contains(&"stable_prefix_tokens_unavailable".to_string())
260+
);
261+
}
262+
235263
#[test]
236264
fn cache_request_facts_populates_provider_thresholds_and_retention_defaults() {
237265
let hot_cache = make_hot_cache(Some(2));

crates/adaptive/tests/unit/config_tests.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,36 @@ fn test_backend_spec_in_memory_helper_uses_empty_config() {
3939
let backend = BackendSpec::in_memory();
4040
assert_eq!(backend.kind, "in_memory");
4141
assert!(backend.config.is_empty());
42+
43+
let default_backend = BackendSpec::default();
44+
assert_eq!(default_backend.kind, "in_memory");
45+
assert!(default_backend.config.is_empty());
46+
}
47+
48+
#[cfg(not(feature = "redis-backend"))]
49+
#[test]
50+
fn test_response_cache_redis_backend_requires_the_redis_feature() {
51+
let mut response_cache = ResponseCacheConfig {
52+
namespace: "cache-tests".to_string(),
53+
..ResponseCacheConfig::default()
54+
};
55+
response_cache.backend.kind = "redis".to_string();
56+
response_cache
57+
.backend
58+
.config
59+
.insert("url".to_string(), json!("redis://127.0.0.1/"));
60+
61+
let report = crate::runtime::features::AdaptiveRuntime::validate_config(&AdaptiveConfig {
62+
response_cache: Some(response_cache),
63+
..AdaptiveConfig::default()
64+
});
65+
66+
assert!(
67+
report
68+
.diagnostics
69+
.iter()
70+
.any(|diagnostic| diagnostic.code == "response_cache.backend_unavailable")
71+
);
4272
}
4373

4474
#[cfg(feature = "redis-backend")]

crates/adaptive/tests/unit/plugin_component_tests.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,42 @@ fn validate_adaptive_plugin_config_reports_component_specific_unknown_fields() {
371371
}));
372372
}
373373

374+
#[test]
375+
fn response_cache_tool_policy_validation_checks_nested_classes_and_overrides() {
376+
let config = json!({
377+
"version": 1,
378+
"response_cache": {
379+
"tools": {
380+
"default": {"unexpected_default": true},
381+
"classes": {
382+
"read_only": {"unexpected_class": true}
383+
},
384+
"overrides": {
385+
"docs_lookup": {"unexpected_override": true}
386+
}
387+
}
388+
},
389+
"policy": {"unknown_field": "warn"}
390+
});
391+
392+
let diagnostics = validate_adaptive_plugin_config(config.as_object().unwrap());
393+
for (component, field) in [
394+
("response_cache.tools.default", "unexpected_default"),
395+
("response_cache.tools.classes.read_only", "unexpected_class"),
396+
(
397+
"response_cache.tools.overrides.docs_lookup",
398+
"unexpected_override",
399+
),
400+
] {
401+
assert!(diagnostics.iter().any(|diagnostic| {
402+
diagnostic.code == "adaptive.unknown_field"
403+
&& diagnostic.component.as_deref() == Some(component)
404+
&& diagnostic.field.as_deref() == Some(field)
405+
&& diagnostic.level == DiagnosticLevel::Warning
406+
}));
407+
}
408+
}
409+
374410
#[tokio::test(flavor = "current_thread")]
375411
async fn adaptive_plugin_registers_runtime_and_rolls_back_registration() {
376412
let _guard = crate::TEST_GLOBAL_CONTEXT_MUTEX.lock().await;

0 commit comments

Comments
 (0)