@@ -41,10 +41,23 @@ public static AssertionResult Evaluate(TestAssertion assertion, AssertionContext
4141 Expected = assertion . Expected
4242 } ;
4343
44+ string ? output = null ;
45+ if ( assertion . Type is AssertionTypes . OutputContains or AssertionTypes . OutputNotContains or AssertionTypes . OutputRegex )
46+ {
47+ var source = OutputSources . Normalize ( assertion . Target ) ;
48+ if ( source == null )
49+ {
50+ result . Passed = false ;
51+ result . Message = OutputSources . InvalidTargetMessage ( assertion ) ;
52+ return result ;
53+ }
54+ output = OutputSources . Read ( source , context ) ;
55+ }
56+
4457 switch ( assertion . Type )
4558 {
4659 case AssertionTypes . OutputContains :
47- result . Actual = context . Output ;
60+ result . Actual = output ;
4861 if ( string . IsNullOrEmpty ( assertion . Expected ) )
4962 {
5063 // Contains("") is vacuously true for any non-null string -- a blank/omitted
@@ -55,21 +68,21 @@ public static AssertionResult Evaluate(TestAssertion assertion, AssertionContext
5568 }
5669 else
5770 {
58- result . Passed = context . Output ? . Contains ( assertion . Expected ,
71+ result . Passed = output ? . Contains ( assertion . Expected ,
5972 StringComparison . OrdinalIgnoreCase ) == true ;
6073 if ( ! result . Passed ) result . Message = "output does not contain the expected text" ;
6174 }
6275 break ;
6376
6477 case AssertionTypes . OutputNotContains :
65- result . Actual = context . Output ;
66- result . Passed = context . Output ? . Contains ( assertion . Expected ?? string . Empty ,
78+ result . Actual = output ;
79+ result . Passed = output ? . Contains ( assertion . Expected ?? string . Empty ,
6780 StringComparison . OrdinalIgnoreCase ) != true ;
6881 if ( ! result . Passed ) result . Message = "output contains text that should not appear" ;
6982 break ;
7083
7184 case AssertionTypes . OutputRegex :
72- result . Actual = context . Output ;
85+ result . Actual = output ;
7386 if ( string . IsNullOrEmpty ( assertion . Expected ) )
7487 {
7588 // An empty pattern matches everything -- a blank/omitted Expected must not
@@ -80,7 +93,7 @@ public static AssertionResult Evaluate(TestAssertion assertion, AssertionContext
8093 }
8194 try
8295 {
83- result . Passed = Regex . IsMatch ( context . Output ?? string . Empty , assertion . Expected ,
96+ result . Passed = Regex . IsMatch ( output ?? string . Empty , assertion . Expected ,
8497 RegexOptions . IgnoreCase , TimeSpan . FromSeconds ( 1 ) ) ;
8598 if ( ! result . Passed ) result . Message = "output does not match the pattern" ;
8699 }
@@ -338,6 +351,39 @@ public static class AgentChainModes
338351 : All . FirstOrDefault ( m => string . Equals ( m , value . Trim ( ) , StringComparison . OrdinalIgnoreCase ) ) ;
339352}
340353
354+ /// <summary>
355+ /// Which text outputContains / outputNotContains / outputRegex and llmJudge read, selected by
356+ /// <see cref="TestAssertion.Target"/>.
357+ ///
358+ /// Reply -- the text a user reads. The default.
359+ /// RichContent -- the reply's rich message as JSON, for content that exists only there, such as an
360+ /// upload card's photo and video lists. Opt-in, so button titles and other structure
361+ /// never leak into the reply text every existing assertion was written against.
362+ /// </summary>
363+ public static class OutputSources
364+ {
365+ public const string Reply = "reply" ;
366+ public const string RichContent = "richContent" ;
367+
368+ public static readonly string [ ] All = [ Reply , RichContent ] ;
369+
370+ /// <summary>
371+ /// Canonical source for any casing, with blank meaning <see cref="Reply"/>; null for an
372+ /// unrecognised value, so a typo fails loudly instead of silently reading the reply text.
373+ /// </summary>
374+ public static string ? Normalize ( string ? value )
375+ => string . IsNullOrWhiteSpace ( value )
376+ ? Reply
377+ : All . FirstOrDefault ( s => string . Equals ( s , value . Trim ( ) , StringComparison . OrdinalIgnoreCase ) ) ;
378+
379+ public static string ? Read ( string source , AssertionContext context )
380+ => source == RichContent ? context . RichContent : context . Output ;
381+
382+ public static string InvalidTargetMessage ( TestAssertion assertion )
383+ => $ "{ assertion . Type } 'target' must be one of { string . Join ( ", " , All ) } "
384+ + $ "(or empty for { Reply } ), not '{ assertion . Target } '";
385+ }
386+
341387/// <summary>
342388/// Save-time counterpart to the four fixed <see cref="AssertionEvaluator"/> branches above
343389/// (outputContains/outputRegex/toolNotCalled/routedToAgent): an assertion missing the one field
0 commit comments