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,202 @@ 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+ Current timestamp is {{ DateTimeOffset . UtcNow . ToUnixTimeMilliseconds ( ) }} .
174+
175+ Please answer the following questions using the appropriate tools:
176+ 1. What is today's date? (use get_date tool)
177+ 2. What is the current price of Tesla (TSLA) stock? (use Yahoo news web search)
178+ 3. Calculate the compound interest for $5,000 at 4% annually for 5 years (use code interpreter)
179+ 4. What is the latest release version of the devlooped/GrokClient repository? (use GitHub MCP tool)
180+
181+ Respond with a JSON object in this exact format:
182+ {
183+ "today": "[date from get_date in YYYY-MM-DD format]",
184+ "tesla_price": [numeric price from web search],
185+ "compound_interest": [numeric result from code interpreter],
186+ "latest_release": "[version string from GitHub]"
187+ }
188+ """
189+ }
190+ } ;
191+
192+ static ( IChatClient grok , GrokChatOptions options , Func < int > getDateCalls ) SetupIntegrationTest ( )
193+ {
194+ var getDateCalls = 0 ;
195+ var grok = new GrokClient ( Environment . GetEnvironmentVariable ( "CI_XAI_API_KEY" ) ! )
196+ . AsIChatClient ( "grok-4-1-fast" )
197+ . AsBuilder ( )
198+ . UseFunctionInvocation ( )
199+ . Build ( ) ;
200+
201+ var options = new GrokChatOptions
202+ {
203+ Include =
204+ [
205+ IncludeOption . InlineCitations ,
206+ IncludeOption . WebSearchCallOutput ,
207+ IncludeOption . CodeExecutionCallOutput ,
208+ IncludeOption . McpCallOutput
209+ ] ,
210+ Tools =
211+ [
212+ // Client-side tool
213+ AIFunctionFactory . Create ( ( ) =>
214+ {
215+ getDateCalls ++ ;
216+ return DateTime . Now . ToString ( "yyyy-MM-dd" ) ;
217+ } , "get_date" , "Gets the current date in YYYY-MM-DD format" ) ,
218+
219+ // Hosted web search tool
220+ new HostedWebSearchTool ( ) ,
221+
222+ // Hosted code interpreter tool
223+ new HostedCodeInterpreterTool ( ) ,
224+
225+ // Hosted MCP server tool (GitHub)
226+ new HostedMcpServerTool ( "GitHub" , "https://api.githubcopilot.com/mcp/" )
227+ {
228+ AuthorizationToken = Environment . GetEnvironmentVariable ( "GITHUB_TOKEN" ) ! ,
229+ AllowedTools = [ "list_releases" , "get_release_by_tag" ] ,
230+ }
231+ ]
232+ } ;
233+
234+ return ( grok , options , ( ) => getDateCalls ) ;
235+ }
236+
237+ void AssertIntegrationTest ( ChatResponse response , Func < int > getDateCalls )
238+ {
239+ // Verify response basics
240+ Assert . NotNull ( response ) ;
241+ Assert . NotNull ( response . ModelId ) ;
242+ Assert . NotEmpty ( response . Messages ) ;
243+
244+ // Verify client-side tool was invoked
245+ Assert . True ( getDateCalls ( ) >= 1 ) ;
246+
247+ // Verify web search tool was used
248+ var webSearchCalls = response . Messages
249+ . SelectMany ( x => x . Contents . Select ( c => c . RawRepresentation as xAI . Protocol . ToolCall ) )
250+ . Where ( x => x ? . Type == xAI . Protocol . ToolCallType . WebSearchTool )
251+ . ToList ( ) ;
252+ Assert . NotEmpty ( webSearchCalls ) ;
253+
254+ // Verify code interpreter tool was used
255+ var codeInterpreterCalls = response . Messages
256+ . SelectMany ( x => x . Contents )
257+ . OfType < CodeInterpreterToolCallContent > ( )
258+ . ToList ( ) ;
259+ Assert . NotEmpty ( codeInterpreterCalls ) ;
260+
261+ // Verify code interpreter output was included
262+ var codeInterpreterResults = response . Messages
263+ . SelectMany ( x => x . Contents )
264+ . OfType < CodeInterpreterToolResultContent > ( )
265+ . ToList ( ) ;
266+ Assert . NotEmpty ( codeInterpreterResults ) ;
267+
268+ // Verify MCP tool was used
269+ var mcpCalls = response . Messages
270+ . SelectMany ( x => x . Contents )
271+ . OfType < McpServerToolCallContent > ( )
272+ . ToList ( ) ;
273+ Assert . NotEmpty ( mcpCalls ) ;
274+
275+ // Verify MCP output was included
276+ var mcpResults = response . Messages
277+ . SelectMany ( x => x . Contents )
278+ . OfType < McpServerToolResultContent > ( )
279+ . ToList ( ) ;
280+ Assert . NotEmpty ( mcpResults ) ;
281+
282+ // Verify citations from web search
283+ Assert . NotEmpty ( response . Messages
284+ . SelectMany ( x => x . Contents )
285+ . SelectMany ( x => x . Annotations ? . OfType < CitationAnnotation > ( ) ?? [ ] )
286+ . Where ( x => x . Url is not null )
287+ . Select ( x => x . Url ! ) ) ;
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 ( $ "Code interpreter calls: { codeInterpreterCalls . Count } ") ;
328+ output . WriteLine ( $ "MCP calls: { mcpCalls . Count } ") ;
329+ }
330+
331+ record IntegrationTestResponse (
332+ string Today ,
333+ decimal TeslaPrice ,
334+ decimal CompoundInterest ,
335+ string LatestRelease ) ;
135336}
0 commit comments