Skip to content

Commit 65ef7e9

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

File tree

3 files changed

+212
-5
lines changed

3 files changed

+212
-5
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ 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 }}
77+
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
7578
shell: pwsh
7679
run: dnx --yes retest -- --no-build
7780

.github/workflows/publish.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ 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 }}
39+
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
3740
shell: pwsh
3841
run: dnx --yes retest -- --no-build
3942

src/xAI.Tests/SanityChecks.cs

Lines changed: 206 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
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()
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

Comments
 (0)