|
14 | 14 | DEFAULT_INVALID_QUESTION_RESPONSE, |
15 | 15 | DEFAULT_MODEL_PROMPT, |
16 | 16 | ) |
| 17 | +from models.common.moderation import ShieldModerationBlocked, ShieldModerationPassed |
17 | 18 | from models.config import ( |
18 | 19 | QuestionValidityConfig, |
19 | 20 | ) |
@@ -532,3 +533,108 @@ async def test_wrap_run_with_sequence_prompt( |
532 | 533 | prompt_str = str(messages[0]) |
533 | 534 | assert "How to" in prompt_str |
534 | 535 | assert "scale a deployment?" in prompt_str |
| 536 | + |
| 537 | + |
| 538 | +class TestQuestionValidityRun: |
| 539 | + """Tests for QuestionValidity.run method.""" |
| 540 | + |
| 541 | + @pytest.fixture(autouse=True) |
| 542 | + def _mock_create_model(self, mocker: MockerFixture) -> None: |
| 543 | + """Mock model creation for all tests.""" |
| 544 | + mocker.patch(f"{_MODULE}.AsyncOgxClientHolder") |
| 545 | + mocker.patch(f"{_MODULE}.OgxResponsesModel.from_ogx_client") |
| 546 | + |
| 547 | + @pytest.mark.asyncio |
| 548 | + async def test_allowed_returns_passed(self, mocker: MockerFixture) -> None: |
| 549 | + """Test that an allowed response returns ShieldModerationPassed.""" |
| 550 | + mock_response = ModelResponse( |
| 551 | + parts=[TextPart(content=SUBJECT_ALLOWED)], |
| 552 | + usage=RequestUsage(input_tokens=10, output_tokens=1), |
| 553 | + ) |
| 554 | + mocker.patch(f"{_MODULE}.model_request", return_value=mock_response) |
| 555 | + |
| 556 | + config = QuestionValidityConfig(model_id="test") |
| 557 | + qv = QuestionValidity(config=config) |
| 558 | + result = await qv.run("How do I create a pod?") |
| 559 | + |
| 560 | + assert isinstance(result, ShieldModerationPassed) |
| 561 | + assert result.decision == "passed" |
| 562 | + |
| 563 | + @pytest.mark.asyncio |
| 564 | + async def test_rejected_returns_blocked(self, mocker: MockerFixture) -> None: |
| 565 | + """Test that a rejected response returns ShieldModerationBlocked.""" |
| 566 | + mock_response = ModelResponse( |
| 567 | + parts=[TextPart(content=SUBJECT_REJECTED)], |
| 568 | + usage=RequestUsage(input_tokens=10, output_tokens=1), |
| 569 | + ) |
| 570 | + mocker.patch(f"{_MODULE}.model_request", return_value=mock_response) |
| 571 | + |
| 572 | + config = QuestionValidityConfig(model_id="test") |
| 573 | + qv = QuestionValidity(config=config) |
| 574 | + result = await qv.run("What is the meaning of life?") |
| 575 | + |
| 576 | + assert isinstance(result, ShieldModerationBlocked) |
| 577 | + assert result.message == DEFAULT_INVALID_QUESTION_RESPONSE |
| 578 | + assert result.moderation_id.startswith("modr-") |
| 579 | + assert result.refusal_response.role == "assistant" |
| 580 | + assert result.refusal_response.content == DEFAULT_INVALID_QUESTION_RESPONSE |
| 581 | + |
| 582 | + @pytest.mark.asyncio |
| 583 | + async def test_unexpected_response_returns_blocked( |
| 584 | + self, mocker: MockerFixture |
| 585 | + ) -> None: |
| 586 | + """Test that an unexpected model response is treated as blocked.""" |
| 587 | + mock_response = ModelResponse( |
| 588 | + parts=[TextPart(content="I don't understand")], |
| 589 | + usage=RequestUsage(input_tokens=10, output_tokens=5), |
| 590 | + ) |
| 591 | + mocker.patch(f"{_MODULE}.model_request", return_value=mock_response) |
| 592 | + |
| 593 | + config = QuestionValidityConfig(model_id="test") |
| 594 | + qv = QuestionValidity(config=config) |
| 595 | + result = await qv.run("some input") |
| 596 | + |
| 597 | + assert isinstance(result, ShieldModerationBlocked) |
| 598 | + assert result.message == DEFAULT_INVALID_QUESTION_RESPONSE |
| 599 | + |
| 600 | + @pytest.mark.asyncio |
| 601 | + @pytest.mark.parametrize( |
| 602 | + "response_text", |
| 603 | + [" ALLOWED", "ALLOWED ", " ALLOWED ", "ALLOWED\n"], |
| 604 | + ids=["leading-space", "trailing-space", "both-spaces", "trailing-newline"], |
| 605 | + ) |
| 606 | + async def test_allowed_with_whitespace_returns_passed( |
| 607 | + self, mocker: MockerFixture, response_text: str |
| 608 | + ) -> None: |
| 609 | + """Test that ALLOWED with surrounding whitespace still returns passed.""" |
| 610 | + mock_response = ModelResponse( |
| 611 | + parts=[TextPart(content=response_text)], |
| 612 | + usage=RequestUsage(input_tokens=10, output_tokens=1), |
| 613 | + ) |
| 614 | + mocker.patch(f"{_MODULE}.model_request", return_value=mock_response) |
| 615 | + |
| 616 | + config = QuestionValidityConfig(model_id="test") |
| 617 | + qv = QuestionValidity(config=config) |
| 618 | + result = await qv.run("How do I scale pods?") |
| 619 | + |
| 620 | + assert isinstance(result, ShieldModerationPassed) |
| 621 | + |
| 622 | + @pytest.mark.asyncio |
| 623 | + async def test_custom_invalid_response_message(self, mocker: MockerFixture) -> None: |
| 624 | + """Test that a custom rejection message is used in the blocked result.""" |
| 625 | + mock_response = ModelResponse( |
| 626 | + parts=[TextPart(content=SUBJECT_REJECTED)], |
| 627 | + usage=RequestUsage(), |
| 628 | + ) |
| 629 | + mocker.patch(f"{_MODULE}.model_request", return_value=mock_response) |
| 630 | + |
| 631 | + config = QuestionValidityConfig( |
| 632 | + model_id="test", invalid_question_response="Custom rejection." |
| 633 | + ) |
| 634 | + qv = QuestionValidity(config=config) |
| 635 | + result = await qv.run("off-topic question") |
| 636 | + |
| 637 | + assert isinstance(result, ShieldModerationBlocked) |
| 638 | + assert result.message == "Custom rejection." |
| 639 | + assert result.refusal_response.role == "assistant" |
| 640 | + assert result.refusal_response.content == "Custom rejection." |
0 commit comments