|
12 | 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | 13 | # See the License for the specific language governing permissions and |
14 | 14 | # limitations under the License. |
| 15 | +import logging |
15 | 16 | import os |
16 | 17 | from unittest.mock import patch |
17 | 18 |
|
18 | | -from nemoguardrails.rails.llm.config import JailbreakDetectionConfig |
| 19 | +import pytest |
| 20 | + |
| 21 | +from nemoguardrails.rails.llm.config import JailbreakDetectionConfig, RailsConfig |
19 | 22 |
|
20 | 23 |
|
21 | 24 | class TestJailbreakDetectionConfig: |
@@ -184,3 +187,247 @@ def test_get_api_key_api_key_env_var_not_set(self): |
184 | 187 |
|
185 | 188 | auth_token = config.get_api_key() |
186 | 189 | assert auth_token is None |
| 190 | + |
| 191 | + def test_negative_length_per_perplexity_threshold_raises(self): |
| 192 | + """Threshold <= 0 should raise ValueError.""" |
| 193 | + with pytest.raises(ValueError, match="greater than 0"): |
| 194 | + JailbreakDetectionConfig(length_per_perplexity_threshold=-1.0) |
| 195 | + |
| 196 | + def test_negative_prefix_suffix_perplexity_threshold_raises(self): |
| 197 | + """Threshold <= 0 should raise ValueError.""" |
| 198 | + with pytest.raises(ValueError, match="greater than 0"): |
| 199 | + JailbreakDetectionConfig(prefix_suffix_perplexity_threshold=0) |
| 200 | + |
| 201 | + def test_invalid_nim_base_url_raises(self): |
| 202 | + """nim_base_url without http(s) scheme should raise ValueError.""" |
| 203 | + with pytest.raises(ValueError, match="nim_base_url must start with"): |
| 204 | + JailbreakDetectionConfig(nim_base_url="ftp://localhost:8000/v1") |
| 205 | + |
| 206 | + def test_invalid_server_endpoint_raises(self): |
| 207 | + """server_endpoint without http(s) scheme should raise ValueError.""" |
| 208 | + with pytest.raises(ValueError, match="server_endpoint must start with"): |
| 209 | + JailbreakDetectionConfig(server_endpoint="localhost:1337/model") |
| 210 | + |
| 211 | + def test_valid_urls_accepted(self): |
| 212 | + """Valid http and https URLs should be accepted.""" |
| 213 | + config = JailbreakDetectionConfig( |
| 214 | + nim_base_url="https://nim.example.com/v1", |
| 215 | + server_endpoint="http://localhost:1337/model", |
| 216 | + ) |
| 217 | + assert config.nim_base_url == "https://nim.example.com/v1" |
| 218 | + assert config.server_endpoint == "http://localhost:1337/model" |
| 219 | + |
| 220 | + |
| 221 | +def _make_rails_config(**kwargs): |
| 222 | + """Helper to build a RailsConfig with minimal required fields.""" |
| 223 | + defaults = { |
| 224 | + "models": [{"type": "main", "engine": "openai", "model": "gpt-3.5-turbo"}], |
| 225 | + } |
| 226 | + defaults.update(kwargs) |
| 227 | + return RailsConfig(**defaults) |
| 228 | + |
| 229 | + |
| 230 | +class TestJailbreakDetectionCrossValidation: |
| 231 | + def test_model_flow_with_nim_url_but_no_endpoint_raises(self): |
| 232 | + """nim_base_url set but nim_server_endpoint empty should raise.""" |
| 233 | + with pytest.raises(Exception, match="nim_server_endpoint is empty"): |
| 234 | + _make_rails_config( |
| 235 | + rails={ |
| 236 | + "input": {"flows": ["jailbreak detection model"]}, |
| 237 | + "config": { |
| 238 | + "jailbreak_detection": { |
| 239 | + "nim_base_url": "http://localhost:8000/v1", |
| 240 | + "nim_server_endpoint": "", |
| 241 | + } |
| 242 | + }, |
| 243 | + }, |
| 244 | + ) |
| 245 | + |
| 246 | + def test_model_flow_with_no_endpoints_warns(self, caplog): |
| 247 | + """No nim_base_url or server_endpoint should warn about local fallback.""" |
| 248 | + with caplog.at_level(logging.WARNING): |
| 249 | + _make_rails_config( |
| 250 | + rails={ |
| 251 | + "input": {"flows": ["jailbreak detection model"]}, |
| 252 | + "config": {"jailbreak_detection": {}}, |
| 253 | + }, |
| 254 | + ) |
| 255 | + assert "No endpoint configured for jailbreak detection model" in caplog.text |
| 256 | + |
| 257 | + def test_heuristics_flow_with_no_server_endpoint_warns(self, caplog): |
| 258 | + """No server_endpoint for heuristics flow should warn.""" |
| 259 | + with caplog.at_level(logging.WARNING): |
| 260 | + _make_rails_config( |
| 261 | + rails={ |
| 262 | + "input": {"flows": ["jailbreak detection heuristics"]}, |
| 263 | + "config": {"jailbreak_detection": {}}, |
| 264 | + }, |
| 265 | + ) |
| 266 | + assert "No server_endpoint configured for jailbreak detection heuristics" in caplog.text |
| 267 | + |
| 268 | + def test_jailbreak_config_present_but_no_flow_warns(self, caplog): |
| 269 | + """Orphaned jailbreak_detection config should warn.""" |
| 270 | + with caplog.at_level(logging.WARNING): |
| 271 | + _make_rails_config( |
| 272 | + rails={ |
| 273 | + "config": { |
| 274 | + "jailbreak_detection": { |
| 275 | + "nim_base_url": "http://localhost:8000/v1", |
| 276 | + } |
| 277 | + }, |
| 278 | + }, |
| 279 | + ) |
| 280 | + assert "no jailbreak detection flow is enabled" in caplog.text |
| 281 | + |
| 282 | + def test_model_flow_with_nim_fully_configured_passes(self, caplog): |
| 283 | + """Fully configured NIM-based model flow should produce no warnings.""" |
| 284 | + with caplog.at_level(logging.WARNING): |
| 285 | + config = _make_rails_config( |
| 286 | + rails={ |
| 287 | + "input": {"flows": ["jailbreak detection model"]}, |
| 288 | + "config": { |
| 289 | + "jailbreak_detection": { |
| 290 | + "nim_base_url": "http://localhost:8000/v1", |
| 291 | + "nim_server_endpoint": "classify", |
| 292 | + } |
| 293 | + }, |
| 294 | + }, |
| 295 | + ) |
| 296 | + assert "jailbreak" not in caplog.text.lower() |
| 297 | + assert config.rails.config.jailbreak_detection.nim_base_url == "http://localhost:8000/v1" |
| 298 | + |
| 299 | + def test_model_flow_with_deprecated_nim_url_no_spurious_warning(self, caplog): |
| 300 | + """Deprecated nim_url/nim_port should not trigger 'no endpoint' warning.""" |
| 301 | + with caplog.at_level(logging.WARNING): |
| 302 | + config = _make_rails_config( |
| 303 | + rails={ |
| 304 | + "input": {"flows": ["jailbreak detection model"]}, |
| 305 | + "config": { |
| 306 | + "jailbreak_detection": { |
| 307 | + "nim_url": "localhost", |
| 308 | + "nim_port": 8000, |
| 309 | + } |
| 310 | + }, |
| 311 | + }, |
| 312 | + ) |
| 313 | + assert "No endpoint configured" not in caplog.text |
| 314 | + # Verify migration happened |
| 315 | + assert config.rails.config.jailbreak_detection.nim_base_url == "http://localhost:8000/v1" |
| 316 | + |
| 317 | + def test_model_flow_with_server_endpoint_passes(self, caplog): |
| 318 | + """Model flow with server_endpoint (no NIM) should pass without warning.""" |
| 319 | + with caplog.at_level(logging.WARNING): |
| 320 | + _make_rails_config( |
| 321 | + rails={ |
| 322 | + "input": {"flows": ["jailbreak detection model"]}, |
| 323 | + "config": { |
| 324 | + "jailbreak_detection": { |
| 325 | + "server_endpoint": "http://localhost:1337/model", |
| 326 | + } |
| 327 | + }, |
| 328 | + }, |
| 329 | + ) |
| 330 | + assert "jailbreak" not in caplog.text.lower() |
| 331 | + |
| 332 | + def test_heuristics_flow_with_server_endpoint_passes(self, caplog): |
| 333 | + """Heuristics flow with server_endpoint should pass without warning.""" |
| 334 | + with caplog.at_level(logging.WARNING): |
| 335 | + _make_rails_config( |
| 336 | + rails={ |
| 337 | + "input": {"flows": ["jailbreak detection heuristics"]}, |
| 338 | + "config": { |
| 339 | + "jailbreak_detection": { |
| 340 | + "server_endpoint": "http://localhost:1337/heuristics", |
| 341 | + } |
| 342 | + }, |
| 343 | + }, |
| 344 | + ) |
| 345 | + assert "jailbreak" not in caplog.text.lower() |
| 346 | + |
| 347 | + def test_model_flow_deprecated_nim_url_empty_server_endpoint_raises(self): |
| 348 | + """Deprecated nim_url with empty nim_server_endpoint should raise.""" |
| 349 | + with pytest.raises(Exception, match="nim_server_endpoint is empty"): |
| 350 | + _make_rails_config( |
| 351 | + rails={ |
| 352 | + "input": {"flows": ["jailbreak detection model"]}, |
| 353 | + "config": { |
| 354 | + "jailbreak_detection": { |
| 355 | + "nim_url": "localhost", |
| 356 | + "nim_server_endpoint": "", |
| 357 | + } |
| 358 | + }, |
| 359 | + }, |
| 360 | + ) |
| 361 | + |
| 362 | + def test_model_flow_nim_port_only_warns(self, caplog): |
| 363 | + """nim_port alone (no nim_url or nim_base_url) should warn about local fallback.""" |
| 364 | + with caplog.at_level(logging.WARNING): |
| 365 | + _make_rails_config( |
| 366 | + rails={ |
| 367 | + "input": {"flows": ["jailbreak detection model"]}, |
| 368 | + "config": { |
| 369 | + "jailbreak_detection": { |
| 370 | + "nim_port": 9000, |
| 371 | + } |
| 372 | + }, |
| 373 | + }, |
| 374 | + ) |
| 375 | + assert "No endpoint configured for jailbreak detection model" in caplog.text |
| 376 | + |
| 377 | + def test_both_flows_nim_only_warns_heuristics(self, caplog): |
| 378 | + """Both flows with only NIM configured should warn for heuristics only.""" |
| 379 | + with caplog.at_level(logging.WARNING): |
| 380 | + _make_rails_config( |
| 381 | + rails={ |
| 382 | + "input": { |
| 383 | + "flows": [ |
| 384 | + "jailbreak detection model", |
| 385 | + "jailbreak detection heuristics", |
| 386 | + ] |
| 387 | + }, |
| 388 | + "config": { |
| 389 | + "jailbreak_detection": { |
| 390 | + "nim_base_url": "http://localhost:8000/v1", |
| 391 | + } |
| 392 | + }, |
| 393 | + }, |
| 394 | + ) |
| 395 | + assert "No endpoint configured for jailbreak detection model" not in caplog.text |
| 396 | + assert "No server_endpoint configured for jailbreak detection heuristics" in caplog.text |
| 397 | + |
| 398 | + def test_both_flows_server_endpoint_only_passes(self, caplog): |
| 399 | + """Both flows with server_endpoint should pass without warnings.""" |
| 400 | + with caplog.at_level(logging.WARNING): |
| 401 | + _make_rails_config( |
| 402 | + rails={ |
| 403 | + "input": { |
| 404 | + "flows": [ |
| 405 | + "jailbreak detection model", |
| 406 | + "jailbreak detection heuristics", |
| 407 | + ] |
| 408 | + }, |
| 409 | + "config": { |
| 410 | + "jailbreak_detection": { |
| 411 | + "server_endpoint": "http://localhost:1337/model", |
| 412 | + } |
| 413 | + }, |
| 414 | + }, |
| 415 | + ) |
| 416 | + assert "jailbreak" not in caplog.text.lower() |
| 417 | + |
| 418 | + def test_explicit_null_jailbreak_detection_config(self, caplog): |
| 419 | + """Explicit None for jailbreak_detection should not raise AttributeError.""" |
| 420 | + with caplog.at_level(logging.WARNING): |
| 421 | + _make_rails_config( |
| 422 | + rails={ |
| 423 | + "input": {"flows": ["jailbreak detection model"]}, |
| 424 | + "config": {"jailbreak_detection": None}, |
| 425 | + }, |
| 426 | + ) |
| 427 | + assert "No endpoint configured for jailbreak detection model" in caplog.text |
| 428 | + |
| 429 | + def test_no_jailbreak_config_no_flow_no_warnings(self, caplog): |
| 430 | + """Default config with no jailbreak config or flows should produce no warnings.""" |
| 431 | + with caplog.at_level(logging.WARNING): |
| 432 | + _make_rails_config() |
| 433 | + assert "jailbreak" not in caplog.text.lower() |
0 commit comments