@@ -13,6 +13,12 @@ public class DetesterBuilder : IDetesterBuilder
1313 private readonly List < string > prompts = [ ] ;
1414 private readonly List < string > expectedResponses = [ ] ;
1515 private readonly List < List < string > > orResponseGroups = [ ] ;
16+ private readonly List < string > unexpectedResponses = [ ] ;
17+ private readonly List < string > unexpectedAnyResponses = [ ] ;
18+ private readonly List < string > regexPatterns = [ ] ;
19+ private readonly List < string > containAllSubstrings = [ ] ;
20+ private readonly List < List < string > > containAnyGroups = [ ] ;
21+ private readonly List < EqualityExpectation > equalityExpectations = [ ] ;
1622 private readonly List < FunctionCallExpectation > expectedFunctionCalls = [ ] ;
1723 private readonly List < JsonExpectation > jsonExpectations = [ ] ;
1824 private string ? instruction ;
@@ -144,6 +150,119 @@ public IDetesterBuilder ShouldContainResponse(string expectedText)
144150 return this ;
145151 }
146152
153+ /// <inheritdoc/>
154+ public IDetesterBuilder ShouldNotContainResponse ( string unexpectedText )
155+ {
156+ if ( string . IsNullOrWhiteSpace ( unexpectedText ) )
157+ {
158+ throw new ArgumentException ( "Unexpected text cannot be null or whitespace." , nameof ( unexpectedText ) ) ;
159+ }
160+
161+ unexpectedResponses . Add ( unexpectedText ) ;
162+ return this ;
163+ }
164+
165+ /// <inheritdoc/>
166+ public IDetesterBuilder ShouldNotContainAnyResponse ( params string [ ] unexpectedTexts )
167+ {
168+ if ( unexpectedTexts == null || unexpectedTexts . Length == 0 )
169+ {
170+ throw new ArgumentException ( "Unexpected texts cannot be null or empty." , nameof ( unexpectedTexts ) ) ;
171+ }
172+
173+ foreach ( var text in unexpectedTexts )
174+ {
175+ if ( string . IsNullOrWhiteSpace ( text ) )
176+ {
177+ throw new ArgumentException ( "Individual unexpected texts cannot be null or whitespace." , nameof ( unexpectedTexts ) ) ;
178+ }
179+
180+ unexpectedAnyResponses . Add ( text ) ;
181+ }
182+
183+ return this ;
184+ }
185+
186+ /// <inheritdoc/>
187+ public IDetesterBuilder ShouldMatchRegex ( string pattern )
188+ {
189+ if ( string . IsNullOrWhiteSpace ( pattern ) )
190+ {
191+ throw new ArgumentException ( "Pattern cannot be null or whitespace." , nameof ( pattern ) ) ;
192+ }
193+
194+ regexPatterns . Add ( pattern ) ;
195+ return this ;
196+ }
197+
198+ /// <inheritdoc/>
199+ public IDetesterBuilder ShouldNotContain ( string unexpectedText )
200+ {
201+ return ShouldNotContainResponse ( unexpectedText ) ;
202+ }
203+
204+ /// <inheritdoc/>
205+ public IDetesterBuilder ShouldContainAll ( params string [ ] expectedSubstrings )
206+ {
207+ if ( expectedSubstrings == null || expectedSubstrings . Length == 0 )
208+ {
209+ throw new ArgumentException ( "Expected substrings cannot be null or empty." , nameof ( expectedSubstrings ) ) ;
210+ }
211+
212+ foreach ( var substring in expectedSubstrings )
213+ {
214+ if ( string . IsNullOrWhiteSpace ( substring ) )
215+ {
216+ throw new ArgumentException ( "Individual expected substrings cannot be null or whitespace." , nameof ( expectedSubstrings ) ) ;
217+ }
218+
219+ containAllSubstrings . Add ( substring ) ;
220+ }
221+
222+ return this ;
223+ }
224+
225+ /// <inheritdoc/>
226+ public IDetesterBuilder ShouldContainAny ( params string [ ] expectedSubstrings )
227+ {
228+ if ( expectedSubstrings == null || expectedSubstrings . Length == 0 )
229+ {
230+ throw new ArgumentException ( "Expected substrings cannot be null or empty." , nameof ( expectedSubstrings ) ) ;
231+ }
232+
233+ var group = new List < string > ( ) ;
234+
235+ foreach ( var substring in expectedSubstrings )
236+ {
237+ if ( string . IsNullOrWhiteSpace ( substring ) )
238+ {
239+ throw new ArgumentException ( "Individual expected substrings cannot be null or whitespace." , nameof ( expectedSubstrings ) ) ;
240+ }
241+
242+ group . Add ( substring ) ;
243+ }
244+
245+ containAnyGroups . Add ( group ) ;
246+ return this ;
247+ }
248+
249+ /// <inheritdoc/>
250+ public IDetesterBuilder ShouldBeEqualTo ( string expected , StringComparison comparison = StringComparison . OrdinalIgnoreCase )
251+ {
252+ if ( expected is null )
253+ {
254+ throw new ArgumentNullException ( nameof ( expected ) ) ;
255+ }
256+
257+ equalityExpectations . Add ( new EqualityExpectation
258+ {
259+ Expected = expected ,
260+ Comparison = comparison ,
261+ } ) ;
262+
263+ return this ;
264+ }
265+
147266 /// <inheritdoc/>
148267 public IDetesterBuilder OrShouldContainResponse ( string expectedText )
149268 {
@@ -248,8 +367,11 @@ public async Task AssertAsync(CancellationToken cancellationToken = default)
248367
249368 chatHistory . Add ( new ChatMessage ( ChatRole . Assistant , response . Text ) ) ;
250369
251- // Check if response contains expected text for any of the assertions
252- if ( expectedResponses . Count > 0 || orResponseGroups . Count > 0 )
370+ // Check if response contains expected or unexpected text for any of the assertions
371+ if ( expectedResponses . Count > 0 || orResponseGroups . Count > 0 ||
372+ unexpectedResponses . Count > 0 || unexpectedAnyResponses . Count > 0 ||
373+ regexPatterns . Count > 0 || containAllSubstrings . Count > 0 ||
374+ containAnyGroups . Count > 0 || equalityExpectations . Count > 0 )
253375 {
254376 var responseText = response . Text ?? string . Empty ;
255377
@@ -266,6 +388,79 @@ public async Task AssertAsync(CancellationToken cancellationToken = default)
266388 $ "Actual response: { responseText } ") ;
267389 }
268390
391+ // Check individual NOT-CONTAINS assertions
392+ var violatingUnexpected = unexpectedResponses
393+ . Where ( unexpected => responseText . Contains ( unexpected , StringComparison . OrdinalIgnoreCase ) )
394+ . ToList ( ) ;
395+
396+ if ( violatingUnexpected . Count > 0 )
397+ {
398+ var violatingText = string . Join ( ", " , violatingUnexpected . Select ( e => $ "'{ e } '") ) ;
399+ throw new DetesterException (
400+ $ "Response contained unexpected text(s): { violatingText } . " +
401+ $ "Actual response: { responseText } ") ;
402+ }
403+
404+ // Check NOT-CONTAINS-ANY assertions (ensure all specified texts are absent)
405+ var violatingAny = unexpectedAnyResponses
406+ . Where ( unexpected => responseText . Contains ( unexpected , StringComparison . OrdinalIgnoreCase ) )
407+ . ToList ( ) ;
408+
409+ if ( violatingAny . Count > 0 )
410+ {
411+ var violatingText = string . Join ( ", " , violatingAny . Select ( e => $ "'{ e } '") ) ;
412+ throw new DetesterException (
413+ $ "Response contained one or more texts that should not appear: { violatingText } . " +
414+ $ "Actual response: { responseText } ") ;
415+ }
416+
417+ // Check regex patterns
418+ foreach ( var pattern in regexPatterns )
419+ {
420+ if ( ! System . Text . RegularExpressions . Regex . IsMatch ( responseText , pattern ) )
421+ {
422+ throw new DetesterException (
423+ $ "Response did not match the required regular expression pattern '{ pattern } '. " +
424+ $ "Actual response: { responseText } ") ;
425+ }
426+ }
427+
428+ // Check that response contains all required substrings
429+ var missingAllSubstrings = containAllSubstrings
430+ . Where ( s => ! responseText . Contains ( s , StringComparison . OrdinalIgnoreCase ) )
431+ . ToList ( ) ;
432+
433+ if ( missingAllSubstrings . Count > 0 )
434+ {
435+ var missingText = string . Join ( ", " , missingAllSubstrings . Select ( e => $ "'{ e } '") ) ;
436+ throw new DetesterException (
437+ $ "Response did not contain all required substrings: { missingText } . " +
438+ $ "Actual response: { responseText } ") ;
439+ }
440+
441+ // Check ANY-groups (at least one in each group must match)
442+ foreach ( var group in containAnyGroups )
443+ {
444+ var hasAny = group . Any ( s => responseText . Contains ( s , StringComparison . OrdinalIgnoreCase ) ) ;
445+ if ( ! hasAny )
446+ {
447+ var options = string . Join ( "' OR '" , group ) ;
448+ throw new DetesterException (
449+ $ "Response did not contain any of the required alternatives: '{ options } '. " +
450+ $ "Actual response: { responseText } ") ;
451+ }
452+ }
453+
454+ // Check equality expectations
455+ foreach ( var expectation in equalityExpectations )
456+ {
457+ if ( ! string . Equals ( responseText , expectation . Expected , expectation . Comparison ) )
458+ {
459+ throw new DetesterException (
460+ $ "Response was not equal to the expected text. Expected: '{ expectation . Expected } ', Actual: '{ responseText } '.") ;
461+ }
462+ }
463+
269464 // Check OR assertions (at least one in each OR group must match)
270465 foreach ( var orGroup in orResponseGroups )
271466 {
0 commit comments