using System.Text.Json; using FluentAssertions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using OpenClawNet.Models.Abstractions; using OpenClawNet.Models.AzureOpenAI; namespace OpenClawNet.UnitTests.Models; /// /// Live tests that call the real Azure OpenAI endpoint. /// Requires user secrets set on the Gateway project: /// dotnet user-secrets set "Model:Provider" "azure-openai" --project src\OpenClawNet.Gateway /// dotnet user-secrets set "Model:Endpoint" "https://YOUR.openai.azure.com/" --project src\OpenClawNet.Gateway /// dotnet user-secrets set "Model:ApiKey" "YOUR-KEY" --project src\OpenClawNet.Gateway /// dotnet user-secrets set "Model:DeploymentName" "gpt-5-mini" --project src\OpenClawNet.Gateway /// /// Run with: dotnet test --filter "Category=Live" /// Skipped automatically when credentials are not configured. /// [Trait("Category", "Live")] public sealed class AzureOpenAILiveTests { // Gateway's UserSecretsId — set by `dotnet user-secrets init` private const string GatewayUserSecretsId = "c15754a6-dc90-4a2a-aecb-1233d1a54fe1"; private readonly AzureOpenAIOptions _options; private readonly bool _isConfigured; public AzureOpenAILiveTests() { var config = new ConfigurationBuilder() .AddUserSecrets(GatewayUserSecretsId, reloadOnChange: false) .Build(); _options = new AzureOpenAIOptions(); if (config["Model:Endpoint"] is { Length: > 0 } ep) _options.Endpoint = ep; if (config["Model:ApiKey"] is { Length: > 0 } key) _options.ApiKey = key; if (config["Model:DeploymentName"] is { Length: > 0 } dep) _options.DeploymentName = dep; if (config["Model:AuthMode"] is { Length: > 0 } mode) _options.AuthMode = mode; _isConfigured = !string.IsNullOrEmpty(_options.Endpoint) && (_options.AuthMode.Equals("integrated", StringComparison.OrdinalIgnoreCase) || !string.IsNullOrEmpty(_options.ApiKey)); } [SkippableFact] public async Task CompleteAsync_ReturnsNonEmptyResponse() { Skip.If(!_isConfigured, "Azure OpenAI credentials not configured — set user secrets to run live tests."); var client = BuildClient(); var response = await client.CompleteAsync(new ChatRequest { Messages = [ new ChatMessage { Role = ChatMessageRole.System, Content = "You are a helpful assistant. Be brief." }, new ChatMessage { Role = ChatMessageRole.User, Content = "Say hello in exactly one sentence." } ] }); response.Content.Should().NotBeNullOrWhiteSpace("the model should return a greeting"); response.Role.Should().Be(ChatMessageRole.Assistant); response.Model.Should().NotBeNullOrWhiteSpace(); response.Usage.Should().NotBeNull(); response.Usage!.TotalTokens.Should().BeGreaterThan(0); } [SkippableFact] public async Task StreamAsync_YieldsChunksAndCompletesSuccessfully() { Skip.If(!_isConfigured, "Azure OpenAI credentials not configured — set user secrets to run live tests."); var client = BuildClient(); var chunks = new List(); await foreach (var chunk in client.StreamAsync(new ChatRequest { Messages = [ new ChatMessage { Role = ChatMessageRole.System, Content = "You are a helpful assistant. Be brief." }, new ChatMessage { Role = ChatMessageRole.User, Content = "Count from 1 to 3." } ] })) { chunks.Add(chunk); } chunks.Should().NotBeEmpty("streaming should yield at least one chunk"); var fullContent = string.Concat(chunks.Select(c => c.Content ?? "")); fullContent.Should().NotBeNullOrWhiteSpace("streamed chunks should contain text"); } [SkippableFact] public async Task IsAvailableAsync_ReturnsTrueWhenConfigured() { Skip.If(!_isConfigured, "Azure OpenAI credentials not configured — set user secrets to run live tests."); var client = BuildClient(); var available = await client.IsAvailableAsync(); available.Should().BeTrue("endpoint should be reachable with valid credentials"); } [SkippableFact] public async Task StreamAsync_WithTools_YieldsToolCallChunk() { Skip.If(!_isConfigured, "Azure OpenAI credentials not configured — set user secrets to run live tests."); var client = BuildClient(); var request = new ChatRequest { Messages = [ new ChatMessage { Role = ChatMessageRole.System, Content = "You are a helpful assistant. When the user asks to list files, always use the list_files tool." }, new ChatMessage { Role = ChatMessageRole.User, Content = "List the files in /src" } ], Tools = [ new ToolDefinition { Name = "list_files", Description = "Lists files and directories at the given path", Parameters = JsonDocument.Parse(""" { "type": "object", "properties": { "path": { "type": "string", "description": "The directory path to list" } }, "required": ["path"] } """) } ] }; var chunks = new List(); await foreach (var chunk in client.StreamAsync(request)) { chunks.Add(chunk); } chunks.Should().NotBeEmpty("streaming should yield at least one chunk"); var toolCallChunks = chunks.Where(c => c.ToolCalls is { Count: > 0 }).ToList(); toolCallChunks.Should().NotBeEmpty( "when tools are provided and the prompt asks to use them, the model should return at least one tool call"); var firstToolCall = toolCallChunks[0].ToolCalls![0]; firstToolCall.Name.Should().Be("list_files"); firstToolCall.Id.Should().NotBeNullOrWhiteSpace(); firstToolCall.Arguments.Should().Contain("path"); } private AzureOpenAIModelClient BuildClient() => new(Options.Create(_options), NullLogger.Instance); }