@@ -5,53 +5,131 @@ namespace Sentry.Extensions.AI.Tests;
55
66public class SentryChatClientTests
77{
8+ private class Fixture
9+ {
10+ private SentryOptions Options { get ; }
11+ public ISentryClient Client { get ; }
12+ public IHub Hub { get ; set ; }
13+
14+ public Fixture ( )
15+ {
16+ Options = new SentryOptions
17+ {
18+ Dsn = ValidDsn ,
19+ TracesSampleRate = 1.0 ,
20+ } ;
21+
22+ SentrySdk . Init ( Options ) ;
23+ Hub = SentrySdk . CurrentHub ;
24+ Client = Substitute . For < ISentryClient > ( ) ;
25+ }
26+ }
27+
28+ private readonly Fixture _fixture = new ( ) ;
29+
830 [ Fact ]
931 public async Task CompleteAsync_CallsInnerClient ( )
1032 {
33+ // Arrange
1134 var inner = Substitute . For < IChatClient > ( ) ;
35+ var sentryChatClient = new SentryChatClient ( inner ) ;
1236 var message = new ChatMessage ( ChatRole . Assistant , "ok" ) ;
1337 var chatResponse = new ChatResponse ( message ) ;
1438 inner . GetResponseAsync ( Arg . Any < IList < ChatMessage > > ( ) , Arg . Any < ChatOptions > ( ) , Arg . Any < CancellationToken > ( ) )
1539 . Returns ( Task . FromResult ( chatResponse ) ) ;
1640
17- var sentryChatClient = new SentryChatClient ( inner ) ;
18-
41+ // Act
1942 var res = await sentryChatClient . GetResponseAsync ( [ new ChatMessage ( ChatRole . User , "hi" ) ] ) ;
2043
44+ // Assert
2145 Assert . Equal ( [ message ] , res . Messages ) ;
2246 await inner . Received ( 1 ) . GetResponseAsync ( Arg . Any < IList < ChatMessage > > ( ) , Arg . Any < ChatOptions > ( ) ,
2347 Arg . Any < CancellationToken > ( ) ) ;
2448 }
2549
2650 [ Fact ]
27- public async Task CompleteStreamingAsync_CallsInnerClient ( )
51+ public async Task CompleteStreamingAsync_CallsInnerClient_AndSetsSpanData ( )
2852 {
29- var inner = Substitute . For < IChatClient > ( ) ;
53+ // Arrange - Use Fixture Hub to start transaction
54+ var transaction = _fixture . Hub . StartTransaction ( "test-streaming" , "test" ) ;
3055
56+ _fixture . Hub . ConfigureScope ( scope => scope . Transaction = transaction ) ;
57+ SentrySdk . ConfigureScope ( scope => scope . Transaction = transaction ) ;
58+
59+ var inner = Substitute . For < IChatClient > ( ) ;
3160 inner . GetStreamingResponseAsync ( Arg . Any < IList < ChatMessage > > ( ) , Arg . Any < ChatOptions > ( ) ,
3261 Arg . Any < CancellationToken > ( ) )
3362 . Returns ( CreateTestStreamingUpdatesAsync ( ) ) ;
34-
3563 var client = new SentryChatClient ( inner ) ;
36-
3764 var results = new List < ChatResponseUpdate > ( ) ;
65+
66+ // Act
3867 await foreach ( var update in client . GetStreamingResponseAsync ( [ new ChatMessage ( ChatRole . User , "hi" ) ] ) )
3968 {
4069 results . Add ( update ) ;
4170 }
4271
72+ // Assert
4373 Assert . Equal ( 2 , results . Count ) ;
4474 Assert . Equal ( "Hello" , results [ 0 ] . Text ) ;
4575 Assert . Equal ( " World!" , results [ 1 ] . Text ) ;
46-
4776 inner . Received ( 1 ) . GetStreamingResponseAsync ( Arg . Any < IList < ChatMessage > > ( ) , Arg . Any < ChatOptions > ( ) ,
4877 Arg . Any < CancellationToken > ( ) ) ;
78+ var spans = transaction . Spans ;
79+ var chatSpan = spans . FirstOrDefault ( s => s . Operation == SentryAIConstants . SpanAttributes . ChatOperation ) ;
80+ Assert . NotNull ( chatSpan ) ;
81+ Assert . Equal ( SpanStatus . Ok , chatSpan . Status ) ;
82+ Assert . True ( chatSpan . IsFinished ) ;
83+ Assert . Equal ( "chat" , chatSpan . Data [ SentryAIConstants . SpanAttributes . OperationName ] ) ;
84+ Assert . Equal ( "Hello World!" , chatSpan . Data [ SentryAIConstants . SpanAttributes . ResponseText ] ) ;
85+ }
86+
87+ [ Fact ]
88+ public async Task CompleteStreamingAsync_HandlesErrors_AndFinishesSpanWithException ( )
89+ {
90+ // Arrange
91+ var transaction = _fixture . Hub . StartTransaction ( "test-streaming-error" , "test" ) ;
92+ _fixture . Hub . ConfigureScope ( scope => scope . Transaction = transaction ) ;
93+ SentrySdk . ConfigureScope ( scope => scope . Transaction = transaction ) ;
94+
95+ var inner = Substitute . For < IChatClient > ( ) ;
96+ var expectedException = new InvalidOperationException ( "Streaming failed" ) ;
97+ inner . GetStreamingResponseAsync ( Arg . Any < IList < ChatMessage > > ( ) , Arg . Any < ChatOptions > ( ) ,
98+ Arg . Any < CancellationToken > ( ) )
99+ . Returns ( CreateFailingStreamingUpdatesAsync ( expectedException ) ) ;
100+ var client = new SentryChatClient ( inner ) ;
101+
102+ // Act
103+ var actualException = await Assert . ThrowsAsync < InvalidOperationException > ( async ( ) =>
104+ {
105+ await foreach ( var update in client . GetStreamingResponseAsync ( [ new ChatMessage ( ChatRole . User , "hi" ) ] ) )
106+ {
107+ // Should not reach here due to exception
108+ }
109+ } ) ;
110+
111+ Assert . Equal ( expectedException . Message , actualException . Message ) ;
112+
113+ // Assert
114+ var spans = transaction . Spans ;
115+ var chatSpan = spans . FirstOrDefault ( s => s . Operation == SentryAIConstants . SpanAttributes . ChatOperation ) ;
116+ Assert . NotNull ( chatSpan ) ;
117+ Assert . Equal ( SpanStatus . InternalError , chatSpan . Status ) ;
118+ Assert . True ( chatSpan . IsFinished ) ;
119+ Assert . Equal ( "chat" , chatSpan . Data [ SentryAIConstants . SpanAttributes . OperationName ] ) ;
120+ }
121+
122+ private static async IAsyncEnumerable < ChatResponseUpdate > CreateFailingStreamingUpdatesAsync ( Exception exception )
123+ {
124+ yield return new ChatResponseUpdate ( ChatRole . System , "Hello" ) ;
125+ await Task . Yield ( ) ;
126+ throw exception ;
49127 }
50128
51129 private static async IAsyncEnumerable < ChatResponseUpdate > CreateTestStreamingUpdatesAsync ( )
52130 {
53131 yield return new ChatResponseUpdate ( ChatRole . System , "Hello" ) ;
54- await Task . Yield ( ) ; // Make it async
132+ await Task . Yield ( ) ;
55133 yield return new ChatResponseUpdate ( ChatRole . System , " World!" ) ;
56134 }
57135}
0 commit comments