Skip to content

Commit f230f98

Browse files
committed
Run sanity/integration tests in CI
1 parent ff461e5 commit f230f98

File tree

3 files changed

+212
-3
lines changed

3 files changed

+212
-3
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ jobs:
7272
run: dotnet build -m:1 -bl:build.binlog
7373

7474
- name: 🧪 test
75+
env:
76+
CI_XAI_API_KEY: ${{ secrets.CI_XAI_API_KEY }}
7577
shell: pwsh
7678
run: dnx --yes retest -- --no-build
7779

.github/workflows/publish.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ jobs:
3434
run: dotnet build -m:1 -bl:build.binlog
3535

3636
- name: 🧪 test
37+
env:
38+
CI_XAI_API_KEY: ${{ secrets.CI_XAI_API_KEY }}
3739
shell: pwsh
3840
run: dnx --yes retest -- --no-build
3941

src/xAI.Tests/SanityChecks.cs

Lines changed: 208 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
using System.Text.Json;
2+
using Devlooped.Extensions.AI;
3+
using Microsoft.Extensions.AI;
24
using Microsoft.Extensions.DependencyInjection;
35
using xAI.Protocol;
46
using Xunit.Abstractions;
7+
using ChatConversation = Devlooped.Extensions.AI.Chat;
58

69
namespace xAI.Tests;
710

811
public 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()
@@ -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()
3134
.AddxAIProtocol(Environment.GetEnvironmentVariable("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 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("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

Comments
 (0)