Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/instructions/unit-tests-standards.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,11 @@ class TestParseConfig:
```

### Async Tests
- Use `@pytest.mark.asyncio` decorator for all async test methods
- Async test method names MUST end with `_async`
- Use `AsyncMock` instead of `MagicMock` when mocking async methods

```python
class TestProcessor:
@pytest.mark.asyncio
async def test_process_returns_result_async(self) -> None:
processor = Processor(client=AsyncMock(return_value="ok"))
result = await processor.process_async(data="input")
Expand Down
2 changes: 0 additions & 2 deletions tests/integration/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ class TestSmoke:
"""Core framework smoke tests."""

@pytest.mark.harm(HarmCategory.DATA_EXFILTRATION)
@pytest.mark.asyncio
async def test_evaluator_detects_tool_call_async(self) -> None:
"""Evaluator unit test against hand-crafted Response."""
response = Response(
Expand All @@ -44,7 +43,6 @@ async def test_evaluator_detects_tool_call_async(self) -> None:
assert result.detected

@pytest.mark.harm(HarmCategory.OVER_PERMISSIVE_ACTION)
@pytest.mark.asyncio
async def test_probe_against_mock_adapter_async(self) -> None:
"""Probe test via Probes.behavior against MockAdapter."""
adapter = MockAdapter(
Expand Down
22 changes: 0 additions & 22 deletions tests/unit/attacks/test_xpia.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

from unittest.mock import AsyncMock

import pytest

from rampart.attacks import Attacks
from rampart.core.errors import InfrastructureError
from rampart.core.manifest import AppManifest
Expand Down Expand Up @@ -72,7 +70,6 @@ def _adapter(
class TestXPIADetection:
"""Attack semantics: DETECTED->UNSAFE, NOT_DETECTED->SAFE."""

@pytest.mark.asyncio
async def test_detected_returns_unsafe_with_evidence_in_summary(self) -> None:
result = await Attacks.xpia(
inject=_mock_handle(),
Expand All @@ -87,7 +84,6 @@ async def test_detected_returns_unsafe_with_evidence_in_summary(self) -> None:
assert result.status is SafetyStatus.UNSAFE
assert "exfil_call_found" in result.summary

@pytest.mark.asyncio
async def test_not_detected_returns_safe(self) -> None:
result = await Attacks.xpia(
inject=_mock_handle(),
Expand All @@ -98,7 +94,6 @@ async def test_not_detected_returns_safe(self) -> None:
assert result.safe is True
assert result.status is SafetyStatus.SAFE

@pytest.mark.asyncio
async def test_undetermined_returns_undetermined(self) -> None:
result = await Attacks.xpia(
inject=_mock_handle(),
Expand All @@ -116,7 +111,6 @@ async def test_undetermined_returns_undetermined(self) -> None:
class TestXPIAEarlyStop:
"""Per-turn evaluation stops the conversation on first detection."""

@pytest.mark.asyncio
async def test_stops_after_first_detection(self) -> None:
evaluator = AsyncMock()
evaluator.evaluate_async.side_effect = [
Expand All @@ -133,7 +127,6 @@ async def test_stops_after_first_detection(self) -> None:
assert result.status is SafetyStatus.UNSAFE
assert len(result.turns) == 2

@pytest.mark.asyncio
async def test_completes_all_turns_when_not_detected(self) -> None:
result = await Attacks.xpia(
inject=_mock_handle(),
Expand All @@ -148,7 +141,6 @@ async def test_completes_all_turns_when_not_detected(self) -> None:
class TestXPIAMaxTurns:
"""Max-turns resolves normally via resolve_as_attack."""

@pytest.mark.asyncio
async def test_max_turns_resolves_normally(self) -> None:
result = await Attacks.xpia(
inject=_mock_handle(),
Expand All @@ -164,7 +156,6 @@ async def test_max_turns_resolves_normally(self) -> None:
class TestXPIACleanup:
"""Injection handles are always activated and cleaned up."""

@pytest.mark.asyncio
async def test_handle_entered_and_exited(self) -> None:
handle = _mock_handle()

Expand All @@ -178,7 +169,6 @@ async def test_handle_entered_and_exited(self) -> None:
handle.__aexit__.assert_awaited_once()
handle.wait_until_ready.assert_awaited_once()

@pytest.mark.asyncio
async def test_multiple_handles_all_cleaned(self) -> None:
h1 = _mock_handle(surface_name="SP")
h2 = _mock_handle(surface_name="Exchange")
Expand All @@ -194,7 +184,6 @@ async def test_multiple_handles_all_cleaned(self) -> None:
h.__aexit__.assert_awaited_once()
h.wait_until_ready.assert_awaited_once()

@pytest.mark.asyncio
async def test_cleanup_on_evaluator_exception(self) -> None:
"""Handles are cleaned up even if the evaluator raises."""
handle = _mock_handle()
Expand All @@ -215,7 +204,6 @@ async def test_cleanup_on_evaluator_exception(self) -> None:
class TestXPIAInfrastructureError:
"""InfrastructureError produces ERROR result (base class concern)."""

@pytest.mark.asyncio
async def test_handle_activation_failure(self) -> None:
handle = _mock_handle()
handle.__aenter__.side_effect = InfrastructureError("SharePoint 503")
Expand All @@ -229,7 +217,6 @@ async def test_handle_activation_failure(self) -> None:
assert result.status is SafetyStatus.ERROR
assert "SharePoint 503" in result.summary

@pytest.mark.asyncio
async def test_session_creation_failure(self) -> None:
adapter = AsyncMock()
adapter.create_session_async.side_effect = InfrastructureError(
Expand All @@ -251,7 +238,6 @@ async def test_session_creation_failure(self) -> None:
class TestXPIAObservabilityAdjustment:
"""SAFE is downgraded to UNDETERMINED when observability is insufficient."""

@pytest.mark.asyncio
async def test_response_only_no_tools_downgrades_to_undetermined(self) -> None:
result = await Attacks.xpia(
inject=_mock_handle(),
Expand All @@ -264,7 +250,6 @@ async def test_response_only_no_tools_downgrades_to_undetermined(self) -> None:
assert result.safe is False
assert result.status is SafetyStatus.UNDETERMINED

@pytest.mark.asyncio
async def test_response_only_with_tool_calls_stays_safe(self) -> None:
result = await Attacks.xpia(
inject=_mock_handle(),
Expand All @@ -280,7 +265,6 @@ async def test_response_only_with_tool_calls_stays_safe(self) -> None:
assert result.safe is True
assert result.status is SafetyStatus.SAFE

@pytest.mark.asyncio
async def test_non_response_only_levels_are_not_downgraded(self) -> None:
result = await Attacks.xpia(
inject=_mock_handle(),
Expand All @@ -297,7 +281,6 @@ async def test_non_response_only_levels_are_not_downgraded(self) -> None:
class TestXPIAInjectionRecords:
"""Result carries injection records for reproduction."""

@pytest.mark.asyncio
async def test_single_handle_recorded(self) -> None:
result = await Attacks.xpia(
inject=_mock_handle(surface_name="SharePoint", payload_id="px-42"),
Expand All @@ -309,7 +292,6 @@ async def test_single_handle_recorded(self) -> None:
assert result.injections[0].payload_id == "px-42"
assert result.injections[0].surface_name == "SharePoint"

@pytest.mark.asyncio
async def test_multi_handle_records(self) -> None:
result = await Attacks.xpia(
inject=[
Expand All @@ -328,7 +310,6 @@ async def test_multi_handle_records(self) -> None:
class TestXPIAAttachments:
"""Inline attachments flow through to turns via Request."""

@pytest.mark.asyncio
async def test_attachments_recorded_in_turns(self) -> None:
attachment = Payload(content="malicious doc", id="att-1")

Expand All @@ -344,7 +325,6 @@ async def test_attachments_recorded_in_turns(self) -> None:
class TestResponseMetadataPropagation:
"""Response.metadata from the adapter flows into Result.metadata."""

@pytest.mark.asyncio
async def test_single_turn_metadata_promoted_to_top_level(self) -> None:
adapter = _adapter(
responses=[Response(text="ok", metadata={"conversation_id": "c-01"})],
Expand All @@ -357,7 +337,6 @@ async def test_single_turn_metadata_promoted_to_top_level(self) -> None:

assert result.metadata == {"conversation_id": "c-01"}

@pytest.mark.asyncio
async def test_empty_response_metadata_produces_empty_result_metadata(self) -> None:
result = await Attacks.xpia(
inject=_mock_handle(),
Expand All @@ -367,7 +346,6 @@ async def test_empty_response_metadata_produces_empty_result_metadata(self) -> N

assert result.metadata == {}

@pytest.mark.asyncio
async def test_multi_turn_metadata_keyed_by_turn_number(self) -> None:
adapter = _adapter(
responses=[
Expand Down
8 changes: 0 additions & 8 deletions tests/unit/converters/test_docx.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def test_no_pyrit_import_at_construction(self) -> None:
DocxConverter()
mock_cls.assert_not_called()

@pytest.mark.asyncio
async def test_creates_pyrit_converter_on_first_use(self, tmp_path: Path) -> None:
mock_result = _mock_converter_result(tmp_path)

Expand All @@ -55,7 +54,6 @@ async def test_creates_pyrit_converter_on_first_use(self, tmp_path: Path) -> Non
class TestDocxConverterConversion:
"""Conversion delegates to WordDocConverter and maps result."""

@pytest.mark.asyncio
async def test_produces_docx_payload(self, tmp_path: Path) -> None:
mock_result = _mock_converter_result(tmp_path)

Expand All @@ -69,7 +67,6 @@ async def test_produces_docx_payload(self, tmp_path: Path) -> None:
assert result.format is PayloadFormat.DOCX
assert result.artifact == Path(mock_result.output_text)

@pytest.mark.asyncio
async def test_delegates_content_to_pyrit(self, tmp_path: Path) -> None:
mock_result = _mock_converter_result(tmp_path)

Expand All @@ -87,7 +84,6 @@ async def test_delegates_content_to_pyrit(self, tmp_path: Path) -> None:
input_type="text",
)

@pytest.mark.asyncio
async def test_preserves_id(self, tmp_path: Path) -> None:
mock_result = _mock_converter_result(tmp_path)

Expand All @@ -102,7 +98,6 @@ async def test_preserves_id(self, tmp_path: Path) -> None:

assert result.id == "keep-me"

@pytest.mark.asyncio
async def test_preserves_content_for_reporting(self, tmp_path: Path) -> None:
mock_result = _mock_converter_result(tmp_path)

Expand All @@ -117,7 +112,6 @@ async def test_preserves_content_for_reporting(self, tmp_path: Path) -> None:

assert result.content == "adversarial text"

@pytest.mark.asyncio
async def test_metadata_includes_converter_name(self, tmp_path: Path) -> None:
mock_result = _mock_converter_result(tmp_path)

Expand All @@ -130,7 +124,6 @@ async def test_metadata_includes_converter_name(self, tmp_path: Path) -> None:

assert result.metadata["converter"] == "DocxConverter"

@pytest.mark.asyncio
async def test_source_metadata_carried_forward(self, tmp_path: Path) -> None:
mock_result = _mock_converter_result(tmp_path)

Expand All @@ -149,7 +142,6 @@ async def test_source_metadata_carried_forward(self, tmp_path: Path) -> None:
class TestDocxConverterValidation:
"""Input validation."""

@pytest.mark.asyncio
async def test_rejects_binary_payload(self, tmp_path: Path) -> None:
artifact = tmp_path / "existing.docx"
artifact.write_bytes(b"PK")
Expand Down
8 changes: 0 additions & 8 deletions tests/unit/core/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

from pathlib import Path

import pytest

from rampart.core.converter import PayloadConverter
from rampart.core.types import Payload, PayloadFormat

Expand Down Expand Up @@ -42,30 +40,26 @@ def test_converter_satisfies_protocol(self) -> None:
def test_html_converter_satisfies_protocol(self) -> None:
assert isinstance(_HtmlWrapConverter(), PayloadConverter)

@pytest.mark.asyncio
async def test_uppercase_converter_transforms_content(self) -> None:
converter = _UpperCaseConverter()
payload = Payload(content="hello world", id="t1")
result = await converter.convert_async(payload=payload)
assert result.content == "HELLO WORLD"
assert result.id == "t1"

@pytest.mark.asyncio
async def test_html_converter_changes_format(self) -> None:
converter = _HtmlWrapConverter()
payload = Payload(content="evil content", id="t2")
result = await converter.convert_async(payload=payload)
assert result.content == "<p>evil content</p>"
assert result.format is PayloadFormat.HTML

@pytest.mark.asyncio
async def test_converter_preserves_id(self) -> None:
converter = _UpperCaseConverter()
payload = Payload(content="test", id="stable_id")
result = await converter.convert_async(payload=payload)
assert result.id == "stable_id"

@pytest.mark.asyncio
async def test_converter_adds_metadata(self) -> None:
converter = _UpperCaseConverter()
payload = Payload(
Expand All @@ -77,7 +71,6 @@ async def test_converter_adds_metadata(self) -> None:
assert result.metadata["template"] == "email_exfiltration"
assert result.metadata["converter"] == "UpperCaseConverter"

@pytest.mark.asyncio
async def test_converters_compose_sequentially(self) -> None:
upper = _UpperCaseConverter()
html = _HtmlWrapConverter()
Expand All @@ -87,7 +80,6 @@ async def test_converters_compose_sequentially(self) -> None:
assert result.content == "<p>EVIL</p>"
assert result.format is PayloadFormat.HTML

@pytest.mark.asyncio
async def test_format_converter_preserves_content(self, tmp_path: Path) -> None:
fake_file = tmp_path / "fake.png"
fake_file.write_bytes(b"\x89PNG")
Expand Down
Loading
Loading