@@ -264,6 +264,78 @@ async def test_float_scale_threshold_scorer_attributes_result_to_aggregate_not_f
264264 assert "azure_severity" not in score .score_metadata
265265
266266
267+ async def test_float_scale_threshold_scorer_rejects_aggregator_that_does_not_combine ():
268+ """
269+ A by-category aggregator returns one result per category, and the threshold can only be
270+ applied to a single value. It used to be accepted and only the first result was used, so
271+ the verdict came from whichever category sorted first and the others were dropped without
272+ a log: with Hate at 0.0 and Violence at 0.9 the caller received False.
273+ """
274+ from pyrit .score .float_scale .float_scale_score_aggregator import FloatScaleScorerByCategory
275+
276+ memory = MagicMock (MemoryInterface )
277+ mock_identifier = ComponentIdentifier (class_name = "MockScorer" , class_module = "test.mock" )
278+
279+ prompt_id = uuid .uuid4 ()
280+ scorer = MagicMock (spec = MessageFloatScaleScorer )
281+ scorer ._score_nested_async = AsyncMock (
282+ return_value = [
283+ Score (
284+ score_value = value ,
285+ score_type = "float_scale" ,
286+ score_category = [category ],
287+ score_rationale = f"{ category } rationale" ,
288+ score_metadata = None ,
289+ message_piece_id = prompt_id ,
290+ score_value_description = "" ,
291+ scorer_class_identifier = mock_identifier ,
292+ id = uuid .uuid4 (),
293+ )
294+ for category , value in (("Hate" , "0.0" ), ("Violence" , "0.9" ))
295+ ]
296+ )
297+ scorer .get_identifier = MagicMock (return_value = mock_identifier )
298+
299+ with patch .object (CentralMemory , "get_memory_instance" , return_value = memory ):
300+ threshold_scorer = FloatScaleThresholdScorer (
301+ scorer = scorer ,
302+ threshold = 0.5 ,
303+ float_scale_aggregator = FloatScaleScorerByCategory .MAX ,
304+ )
305+
306+ with pytest .raises (RuntimeError , match = "returned 2 results" ) as exc_info :
307+ await threshold_scorer .score_text_async (text = "mock example" )
308+
309+ # The message has to name the aggregator that cannot be thresholded, so the caller
310+ # knows to pick a combining one instead of guessing why no score came back.
311+ assert "FloatScaleScorerByCategory.MAX cannot be thresholded" in str (exc_info .value )
312+ memory .add_scores_to_memory .assert_not_called ()
313+
314+
315+ async def test_float_scale_threshold_scorer_rejects_aggregator_that_returns_nothing ():
316+ """An aggregator that returns no result is a configuration error, not an IndexError."""
317+
318+ def empty_aggregator (scores ):
319+ return []
320+
321+ empty_aggregator .__name__ = "empty_aggregator"
322+
323+ memory = MagicMock (MemoryInterface )
324+ scorer = create_mock_float_scorer (0.9 )
325+
326+ with patch .object (CentralMemory , "get_memory_instance" , return_value = memory ):
327+ threshold_scorer = FloatScaleThresholdScorer (
328+ scorer = scorer ,
329+ threshold = 0.5 ,
330+ float_scale_aggregator = empty_aggregator ,
331+ )
332+
333+ with pytest .raises (RuntimeError , match = "empty_aggregator returned 0 results" ):
334+ await threshold_scorer .score_text_async (text = "mock example" )
335+
336+ memory .add_scores_to_memory .assert_not_called ()
337+
338+
267339async def test_float_scale_threshold_scorer_single_score_attribution_unchanged ():
268340 """A single wrapped score must keep its own category and rationale, as before."""
269341
0 commit comments