11using System . Text . Json ;
2+ using Devlooped . Extensions . AI ;
3+ using Microsoft . Extensions . AI ;
24using Microsoft . Extensions . DependencyInjection ;
35using xAI . Protocol ;
46using Xunit . Abstractions ;
7+ using ChatConversation = Devlooped . Extensions . AI . Chat ;
58
69namespace xAI . Tests ;
710
811public class SanityChecks ( ITestOutputHelper output )
912{
10- [ SecretsFact ( "XAI_API_KEY " ) ]
13+ [ SecretsFact ( "CI_XAI_API_KEY " ) ]
1114 public async Task ListModelsAsync ( )
1215 {
1316 var services = new ServiceCollection ( )
14- . AddxAIProtocol ( Environment . GetEnvironmentVariable ( "XAI_API_KEY " ) ! )
17+ . AddxAIProtocol ( Environment . GetEnvironmentVariable ( "CI_XAI_API_KEY " ) ! )
1518 . BuildServiceProvider ( ) ;
1619
1720 var client = services . GetRequiredService < Models . ModelsClient > ( ) ;
@@ -24,14 +27,14 @@ public async Task ListModelsAsync()
2427 output . WriteLine ( model . Name ) ;
2528 }
2629
27- [ SecretsFact ( "XAI_API_KEY " ) ]
30+ [ SecretsFact ( "CI_XAI_API_KEY " ) ]
2831 public async Task ExecuteLocalFunctionWithWebSearch ( )
2932 {
3033 var services = new ServiceCollection ( )
31- . AddxAIProtocol ( Environment . GetEnvironmentVariable ( "XAI_API_KEY " ) ! )
34+ . AddxAIProtocol ( Environment . GetEnvironmentVariable ( "CI_XAI_API_KEY " ) ! )
3235 . BuildServiceProvider ( ) ;
3336
34- var client = services . GetRequiredService < Chat . ChatClient > ( ) ;
37+ var client = services . GetRequiredService < xAI . Protocol . Chat . ChatClient > ( ) ;
3538
3639 // Define a local function to get the current date
3740 var getDateFunction = new Function
@@ -132,4 +135,206 @@ public async Task ExecuteLocalFunctionWithWebSearch()
132135 Assert . NotNull ( finalOutput . Message . Content ) ;
133136 Assert . NotEmpty ( finalOutput . Message . Content ) ;
134137 }
138+
139+ /// <summary>
140+ /// Comprehensive integration test (non-streaming) that exercises all major features:
141+ /// - Client-side tool invocation (AIFunctionFactory)
142+ /// - Hosted web search tool
143+ /// - Hosted code interpreter tool
144+ /// - Hosted MCP server tool (GitHub)
145+ /// - Citations and annotations
146+ /// </summary>
147+ [ SecretsFact ( "CI_XAI_API_KEY" , "GITHUB_TOKEN" ) ]
148+ public async Task IntegrationTest ( )
149+ {
150+ var ( grok , options , getDateCalls ) = SetupIntegrationTest ( ) ;
151+
152+ var response = await grok . GetResponseAsync ( CreateIntegrationChat ( ) , options ) ;
153+
154+ AssertIntegrationTest ( response , getDateCalls ) ;
155+ }
156+
157+ [ SecretsFact ( "CI_XAI_API_KEY" , "GITHUB_TOKEN" ) ]
158+ public async Task IntegrationTestStreaming ( )
159+ {
160+ var ( grok , options , getDateCalls ) = SetupIntegrationTest ( ) ;
161+
162+ var updates = await grok . GetStreamingResponseAsync ( CreateIntegrationChat ( ) , options ) . ToListAsync ( ) ;
163+ var response = updates . ToChatResponse ( ) ;
164+
165+ AssertIntegrationTest ( response , getDateCalls ) ;
166+ }
167+
168+ static ChatConversation CreateIntegrationChat ( ) => new ( )
169+ {
170+ { "system" , "You are a helpful assistant that uses all available tools to answer questions accurately." } ,
171+ { "user" ,
172+ """
173+ Please answer the following questions using the appropriate tools:
174+ 1. What is today's date? (use get_date tool)
175+ 2. What is the current price of Tesla (TSLA) stock? (use Yahoo news web search)
176+ 3. Calculate the compound interest for $5,000 at 4% annually for 5 years (use code interpreter)
177+ 4. What is the latest release version of the devlooped/GrokClient repository? (use GitHub MCP tool)
178+
179+ Respond with a JSON object in this exact format:
180+ {
181+ "today": "[date from get_date in YYYY-MM-DD format]",
182+ "tesla_price": [numeric price from web search],
183+ "compound_interest": [numeric result from code interpreter],
184+ "latest_release": "[version string from GitHub]"
185+ }
186+ """
187+ }
188+ } ;
189+
190+ static ( IChatClient grok , GrokChatOptions options , Func < int > getDateCalls ) SetupIntegrationTest ( )
191+ {
192+ var getDateCalls = 0 ;
193+ var grok = new GrokClient ( Environment . GetEnvironmentVariable ( "CI_XAI_API_KEY" ) ! )
194+ . AsIChatClient ( "grok-4-fast" )
195+ . AsBuilder ( )
196+ . UseFunctionInvocation ( )
197+ . Build ( ) ;
198+
199+ var options = new GrokChatOptions
200+ {
201+ Include =
202+ [
203+ IncludeOption . InlineCitations ,
204+ IncludeOption . WebSearchCallOutput ,
205+ IncludeOption . CodeExecutionCallOutput ,
206+ IncludeOption . McpCallOutput
207+ ] ,
208+ Tools =
209+ [
210+ // Client-side tool
211+ AIFunctionFactory . Create ( ( ) =>
212+ {
213+ getDateCalls ++ ;
214+ return DateTime . Now . ToString ( "yyyy-MM-dd" ) ;
215+ } , "get_date" , "Gets the current date in YYYY-MM-DD format" ) ,
216+
217+ // Hosted web search tool
218+ new HostedWebSearchTool ( ) ,
219+
220+ // Hosted code interpreter tool
221+ new HostedCodeInterpreterTool ( ) ,
222+
223+ // Hosted MCP server tool (GitHub)
224+ new HostedMcpServerTool ( "GitHub" , "https://api.githubcopilot.com/mcp/" )
225+ {
226+ AuthorizationToken = Environment . GetEnvironmentVariable ( "GITHUB_TOKEN" ) ! ,
227+ AllowedTools = [ "list_releases" , "get_release_by_tag" ] ,
228+ }
229+ ]
230+ } ;
231+
232+ return ( grok , options , ( ) => getDateCalls ) ;
233+ }
234+
235+ void AssertIntegrationTest ( ChatResponse response , Func < int > getDateCalls )
236+ {
237+ // Verify response basics
238+ Assert . NotNull ( response ) ;
239+ Assert . NotNull ( response . ModelId ) ;
240+ Assert . NotEmpty ( response . Messages ) ;
241+
242+ // Verify client-side tool was invoked
243+ Assert . True ( getDateCalls ( ) >= 1 ) ;
244+
245+ // Verify web search tool was used
246+ var webSearchCalls = response . Messages
247+ . SelectMany ( x => x . Contents . Select ( c => c . RawRepresentation as xAI . Protocol . ToolCall ) )
248+ . Where ( x => x ? . Type == xAI . Protocol . ToolCallType . WebSearchTool )
249+ . ToList ( ) ;
250+ Assert . NotEmpty ( webSearchCalls ) ;
251+
252+ // Verify code interpreter tool was used
253+ var codeInterpreterCalls = response . Messages
254+ . SelectMany ( x => x . Contents )
255+ . OfType < CodeInterpreterToolCallContent > ( )
256+ . ToList ( ) ;
257+ Assert . NotEmpty ( codeInterpreterCalls ) ;
258+
259+ // Verify code interpreter output was included
260+ var codeInterpreterResults = response . Messages
261+ . SelectMany ( x => x . Contents )
262+ . OfType < CodeInterpreterToolResultContent > ( )
263+ . ToList ( ) ;
264+ Assert . NotEmpty ( codeInterpreterResults ) ;
265+
266+ // Verify MCP tool was used
267+ var mcpCalls = response . Messages
268+ . SelectMany ( x => x . Contents )
269+ . OfType < McpServerToolCallContent > ( )
270+ . ToList ( ) ;
271+ Assert . NotEmpty ( mcpCalls ) ;
272+
273+ // Verify MCP output was included
274+ var mcpResults = response . Messages
275+ . SelectMany ( x => x . Contents )
276+ . OfType < McpServerToolResultContent > ( )
277+ . ToList ( ) ;
278+ Assert . NotEmpty ( mcpResults ) ;
279+
280+ // Verify citations from web search
281+ var citations = response . Messages
282+ . SelectMany ( x => x . Contents )
283+ . SelectMany ( x => x . Annotations ? . OfType < CitationAnnotation > ( ) ?? [ ] )
284+ . Where ( x => x . Url is not null )
285+ . Select ( x => x . Url ! )
286+ . ToList ( ) ;
287+ Assert . NotEmpty ( citations ) ;
288+
289+ // Parse and validate the JSON response
290+ var responseText = response . Messages . Last ( ) . Text ;
291+ Assert . NotNull ( responseText ) ;
292+
293+ output . WriteLine ( "Response text:" ) ;
294+ output . WriteLine ( responseText ) ;
295+
296+ // Extract JSON from response (may be wrapped in markdown code blocks)
297+ var jsonStart = responseText . IndexOf ( '{' ) ;
298+ var jsonEnd = responseText . LastIndexOf ( '}' ) ;
299+ if ( jsonStart >= 0 && jsonEnd > jsonStart )
300+ {
301+ var json = responseText . Substring ( jsonStart , jsonEnd - jsonStart + 1 ) ;
302+ var result = JsonSerializer . Deserialize < IntegrationTestResponse > ( json , new JsonSerializerOptions ( JsonSerializerDefaults . Web )
303+ {
304+ PropertyNamingPolicy = JsonNamingPolicy . SnakeCaseLower
305+ } ) ;
306+
307+ Assert . NotNull ( result ) ;
308+
309+ // Verify date is today
310+ Assert . Equal ( DateTime . Today . ToString ( "yyyy-MM-dd" ) , result . Today ) ;
311+
312+ // Verify Tesla price is reasonable (greater than $100)
313+ Assert . True ( result . TeslaPrice > 100 , $ "Tesla price { result . TeslaPrice } should be > 100") ;
314+
315+ // Verify compound interest calculation is approximately correct
316+ // Formula: P(1 + r)^t - P = 5000 * (1.04)^5 - 5000 ≈ $1,083.26
317+ Assert . True ( result . CompoundInterest > 1000 && result . CompoundInterest < 1200 ,
318+ $ "Compound interest { result . CompoundInterest } should be between 1000 and 1200") ;
319+
320+ // Verify latest release contains version pattern
321+ Assert . NotNull ( result . LatestRelease ) ;
322+ Assert . Contains ( "." , result . LatestRelease ) ;
323+
324+ output . WriteLine ( $ "Parsed response: Today={ result . Today } , TeslaPrice={ result . TeslaPrice } , CompoundInterest={ result . CompoundInterest } , LatestRelease={ result . LatestRelease } ") ;
325+ }
326+
327+ output . WriteLine ( $ "Web search citations: { citations . Count } ") ;
328+ foreach ( var url in citations . Take ( 5 ) )
329+ output . WriteLine ( $ " - { url } ") ;
330+
331+ output . WriteLine ( $ "Code interpreter calls: { codeInterpreterCalls . Count } ") ;
332+ output . WriteLine ( $ "MCP calls: { mcpCalls . Count } ") ;
333+ }
334+
335+ record IntegrationTestResponse (
336+ string Today ,
337+ decimal TeslaPrice ,
338+ decimal CompoundInterest ,
339+ string LatestRelease ) ;
135340}
0 commit comments