Skip to content
Open
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
59 changes: 59 additions & 0 deletions tests/strands_evals/experimental/redteam/test_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Tests for RedTeamCase metadata sync behavior."""

from strands_evals.experimental.redteam.case import RedTeamCase
from strands_evals.experimental.redteam.types import AttackGoal, RedTeamConfig


def _config(**attack_goal_kwargs) -> RedTeamConfig:
kwargs = {"risk_category": "guideline_bypass", "actor_goal": "goal"}
kwargs.update(attack_goal_kwargs)
return RedTeamConfig(attack_goal=AttackGoal(**kwargs))


def test_metadata_none_is_populated_from_attack_goal():
"""A case with no metadata gets the full attack_goal dump as its metadata."""
case = RedTeamCase(name="c0", input="hello", config=_config())

assert case.metadata == case.config.attack_goal.model_dump()
assert case.metadata["risk_category"] == "guideline_bypass"
assert case.metadata["actor_goal"] == "goal"
assert case.metadata["severity"] == "medium"


def test_existing_metadata_keys_are_preserved_not_overwritten():
"""Caller-provided metadata wins over the attack_goal dump for overlapping keys."""
case = RedTeamCase(
name="c0",
input="hello",
config=_config(severity="critical"),
metadata={"severity": "low", "custom": "value"},
)

# setdefault semantics: caller's "severity" survives, config's does not override it.
assert case.metadata["severity"] == "low"
# Caller-only keys are untouched.
assert case.metadata["custom"] == "value"
# Keys absent from caller metadata are still filled in from attack_goal.
assert case.metadata["risk_category"] == "guideline_bypass"
assert case.metadata["actor_goal"] == "goal"


def test_partial_metadata_is_filled_in_from_attack_goal():
"""Keys the caller didn't set are backfilled from attack_goal without touching the rest."""
case = RedTeamCase(
name="c0",
input="hello",
config=_config(context="ctx", success_criteria="criteria"),
metadata={"unrelated": "kept"},
)

assert case.metadata["unrelated"] == "kept"
assert case.metadata["context"] == "ctx"
assert case.metadata["success_criteria"] == "criteria"


def test_empty_dict_metadata_is_filled_in_from_attack_goal():
"""An explicit empty dict (falsy but not None) still gets backfilled, exercising the else branch."""
case = RedTeamCase(name="c0", input="hello", config=_config(), metadata={})

assert case.metadata == case.config.attack_goal.model_dump()
47 changes: 47 additions & 0 deletions tests/strands_evals/experimental/redteam/test_common.py

@pgrayy pgrayy Jul 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, I would recommend placing this under the directory redteam/strategies/. This also helps directly map the test module to its src module.

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Tests for shared prompt-block assembly in strategies/_common.py."""

from strands_evals.experimental.redteam.strategies._common import (
EXIT_BLOCK,
FORMAT_BLOCK,
PROFILE_BLOCK,
ROLE_BLOCK,
RULES_BLOCK,
_build_system_prompt,
)


def test_build_system_prompt_includes_all_common_blocks():
prompt = _build_system_prompt("PLAYBOOK CONTENT")

assert ROLE_BLOCK in prompt
assert PROFILE_BLOCK in prompt
assert "PLAYBOOK CONTENT" in prompt
assert RULES_BLOCK in prompt
assert EXIT_BLOCK in prompt
assert FORMAT_BLOCK in prompt


def test_build_system_prompt_orders_blocks_role_profile_playbook_rules_exit_format():
"""Downstream .format() calls fill {actor_goal}/{max_turns} placeholders positionally
within this fixed layout, so the block order is load-bearing, not incidental."""
prompt = _build_system_prompt("PLAYBOOK")

positions = [
prompt.index(ROLE_BLOCK),
prompt.index(PROFILE_BLOCK),
prompt.index("PLAYBOOK"),
prompt.index(RULES_BLOCK),
prompt.index(EXIT_BLOCK),
prompt.index(FORMAT_BLOCK),
]

assert positions == sorted(positions)


def test_profile_and_exit_blocks_retain_format_placeholders():
"""These blocks are filled in later via str.format(actor_profile=..., max_turns=...);
_build_system_prompt itself must not consume those placeholders."""
prompt = _build_system_prompt("PLAYBOOK")

assert "{actor_profile}" in prompt
assert "{max_turns}" in prompt
Loading