@@ -25,7 +25,8 @@ use nemo_relay::api::subscriber::{deregister_subscriber, flush_subscribers, regi
2525use nemo_relay:: api:: tool:: { ToolCallExecuteParams , tool_call_execute} ;
2626use nemo_relay:: error:: FlowError ;
2727use 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} ;
3031use nemo_relay_adaptive:: plugin_component:: { ComponentSpec , register_adaptive_component} ;
3132use 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]
631715async 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
0 commit comments