// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics; using System.Reflection; using System.Text; using System.Text.Json; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; namespace Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests; [Collection("Samples")] [Trait("Category", "SampleValidation")] public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLifetime { private const string AzureFunctionsPort = "7071"; private const string AzuritePort = "10000"; private const string DtsPort = "8080"; private const string RedisPort = "6379"; private static readonly string s_dotnetTargetFramework = GetTargetFramework(); #if DEBUG private const string BuildConfiguration = "Debug"; #else private const string BuildConfiguration = "Release"; #endif private static readonly HttpClient s_sharedHttpClient = new(); private static readonly IConfiguration s_configuration = new ConfigurationBuilder() .AddEnvironmentVariables() .AddUserSecrets(Assembly.GetExecutingAssembly()) .Build(); private static bool s_infrastructureStarted; private static readonly TimeSpan s_orchestrationTimeout = TimeSpan.FromMinutes(3); // In CI, `dotnet run` builds the Functions project from scratch before the host starts, so 60s is not enough. private static readonly TimeSpan s_functionsReadyTimeout = TimeSpan.FromSeconds(180); private static readonly string s_samplesPath = Path.GetFullPath( Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", "..", "samples", "04-hosting", "DurableAgents", "AzureFunctions")); private readonly ITestOutputHelper _outputHelper = outputHelper; async ValueTask IAsyncLifetime.InitializeAsync() { if (!s_infrastructureStarted) { await this.StartSharedInfrastructureAsync(); s_infrastructureStarted = true; } } async ValueTask IAsyncDisposable.DisposeAsync() { // Nothing to clean up await Task.CompletedTask; } [RetryFact(2, 5000)] public async Task SingleAgentSampleValidationAsync() { string samplePath = Path.Combine(s_samplesPath, "01_SingleAgent"); await this.RunSampleTestAsync(samplePath, async (logs) => { Uri startUri = new($"http://localhost:{AzureFunctionsPort}/api/agents/Joker/run"); this._outputHelper.WriteLine($"Starting single agent orchestration via POST request to {startUri}..."); // Test the agent endpoint as described in the README const string RequestBody = "Tell me a joke about a pirate."; using HttpContent content = new StringContent(RequestBody, Encoding.UTF8, "text/plain"); using HttpResponseMessage response = await s_sharedHttpClient.PostAsync(startUri, content); // The response is expected to be a plain text response with the agent's reply (the joke) Assert.True(response.IsSuccessStatusCode, $"Agent request failed with status: {response.StatusCode}"); Assert.Equal("text/plain", response.Content.Headers.ContentType?.MediaType); string responseText = await response.Content.ReadAsStringAsync(); Assert.NotEmpty(responseText); this._outputHelper.WriteLine($"Agent run response: {responseText}"); // The response headers should include the agent session ID, which can be used to continue the conversation. string? sessionId = response.Headers.GetValues("x-ms-thread-id")?.FirstOrDefault(); Assert.NotNull(sessionId); Assert.NotEmpty(sessionId); this._outputHelper.WriteLine($"Agent session ID: {sessionId}"); // Wait for up to 30 seconds to see if the agent response is available in the logs await this.WaitForConditionAsync( condition: () => { lock (logs) { bool exists = logs.Any( log => log.Message.Contains("Response:") && log.Message.Contains(sessionId)); return Task.FromResult(exists); } }, message: "Agent response is available", timeout: TimeSpan.FromSeconds(30)); }); } [Fact(Skip = "Flaky: LLM non-determinism can produce null orchestration results")] public async Task SingleAgentOrchestrationChainingSampleValidationAsync() { string samplePath = Path.Combine(s_samplesPath, "02_AgentOrchestration_Chaining"); await this.RunSampleTestAsync(samplePath, async (logs) => { Uri startUri = new($"http://localhost:{AzureFunctionsPort}/api/singleagent/run"); this._outputHelper.WriteLine($"Starting single agent orchestration via POST request to {startUri}..."); // Start the orchestration using HttpResponseMessage startResponse = await s_sharedHttpClient.PostAsync(startUri, content: null); Assert.True( startResponse.IsSuccessStatusCode, $"Start orchestration failed with status: {startResponse.StatusCode}"); string startResponseText = await startResponse.Content.ReadAsStringAsync(); JsonElement startResult = JsonElement.Parse(startResponseText); Assert.True(startResult.TryGetProperty("statusQueryGetUri", out JsonElement statusUriElement)); Uri statusUri = new(statusUriElement.GetString()!); // Wait for orchestration to complete await this.WaitForOrchestrationCompletionAsync(statusUri); // Verify the final result using HttpResponseMessage statusResponse = await s_sharedHttpClient.GetAsync(statusUri); Assert.True( statusResponse.IsSuccessStatusCode, $"Status check failed with status: {statusResponse.StatusCode}"); string statusText = await statusResponse.Content.ReadAsStringAsync(); JsonElement statusResult = JsonElement.Parse(statusText); Assert.Equal("Completed", statusResult.GetProperty("runtimeStatus").GetString()); Assert.True(statusResult.TryGetProperty("output", out JsonElement outputElement)); string? output = outputElement.GetString(); // Can't really validate the output since it's non-deterministic, but we can at least check it's non-empty Assert.NotNull(output); Assert.True(output.Length > 20, "Output is unexpectedly short"); }); } [RetryFact(2, 5000)] public async Task MultiAgentOrchestrationConcurrentSampleValidationAsync() { string samplePath = Path.Combine(s_samplesPath, "03_AgentOrchestration_Concurrency"); await this.RunSampleTestAsync(samplePath, async (logs) => { // Start the multi-agent orchestration const string RequestBody = "What is temperature?"; using HttpContent content = new StringContent(RequestBody, Encoding.UTF8, "text/plain"); Uri startUri = new($"http://localhost:{AzureFunctionsPort}/api/multiagent/run"); this._outputHelper.WriteLine($"Starting multi agent orchestration via POST request to {startUri}..."); using HttpResponseMessage startResponse = await s_sharedHttpClient.PostAsync(startUri, content); Assert.True(startResponse.IsSuccessStatusCode, $"Start orchestration failed with status: {startResponse.StatusCode}"); string startResponseText = await startResponse.Content.ReadAsStringAsync(); JsonElement startResult = JsonElement.Parse(startResponseText); Assert.True(startResult.TryGetProperty("instanceId", out JsonElement instanceIdElement)); Assert.True(startResult.TryGetProperty("statusQueryGetUri", out JsonElement statusUriElement)); Uri statusUri = new(statusUriElement.GetString()!); // Wait for orchestration to complete await this.WaitForOrchestrationCompletionAsync(statusUri); // Verify the final result using HttpResponseMessage statusResponse = await s_sharedHttpClient.GetAsync(statusUri); Assert.True(statusResponse.IsSuccessStatusCode, $"Status check failed with status: {statusResponse.StatusCode}"); string statusText = await statusResponse.Content.ReadAsStringAsync(); JsonElement statusResult = JsonElement.Parse(statusText); Assert.Equal("Completed", statusResult.GetProperty("runtimeStatus").GetString()); Assert.True(statusResult.TryGetProperty("output", out JsonElement outputElement)); // Verify both physicist and chemist responses are present Assert.True(outputElement.TryGetProperty("physicist", out JsonElement physicistElement)); Assert.True(outputElement.TryGetProperty("chemist", out JsonElement chemistElement)); string physicistResponse = physicistElement.GetString()!; string chemistResponse = chemistElement.GetString()!; Assert.NotEmpty(physicistResponse); Assert.NotEmpty(chemistResponse); Assert.Contains("temperature", physicistResponse, StringComparison.OrdinalIgnoreCase); Assert.Contains("temperature", chemistResponse, StringComparison.OrdinalIgnoreCase); }); } [RetryFact(2, 5000)] public async Task MultiAgentOrchestrationConditionalsSampleValidationAsync() { string samplePath = Path.Combine(s_samplesPath, "04_AgentOrchestration_Conditionals"); await this.RunSampleTestAsync(samplePath, async (logs) => { // Test with legitimate email await this.TestSpamDetectionAsync("email-001", "Hi John, I hope you're doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!", expectedSpam: false); // Test with spam email await this.TestSpamDetectionAsync("email-002", "URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!", expectedSpam: true); }); } [RetryFact(2, 5000)] public async Task SingleAgentOrchestrationHITLSampleValidationAsync() { string samplePath = Path.Combine(s_samplesPath, "05_AgentOrchestration_HITL"); await this.RunSampleTestAsync(samplePath, async (logs) => { // Start the HITL orchestration with short timeout for testing // TODO: Add validation for the approval case object requestBody = new { topic = "The Future of Artificial Intelligence", max_review_attempts = 3, approval_timeout_hours = 0.001 // Very short timeout for testing }; string jsonContent = JsonSerializer.Serialize(requestBody); using HttpContent content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); Uri startUri = new($"http://localhost:{AzureFunctionsPort}/api/hitl/run"); this._outputHelper.WriteLine($"Starting HITL orchestration via POST request to {startUri}..."); using HttpResponseMessage startResponse = await s_sharedHttpClient.PostAsync(startUri, content); Assert.True( startResponse.IsSuccessStatusCode, $"Start HITL orchestration failed with status: {startResponse.StatusCode}"); string startResponseText = await startResponse.Content.ReadAsStringAsync(); JsonElement startResult = JsonElement.Parse(startResponseText); Assert.True(startResult.TryGetProperty("statusQueryGetUri", out JsonElement statusUriElement)); Uri statusUri = new(statusUriElement.GetString()!); // Wait for orchestration to complete (it should timeout due to short timeout) await this.WaitForOrchestrationCompletionAsync(statusUri); // Verify the final result using HttpResponseMessage statusResponse = await s_sharedHttpClient.GetAsync(statusUri); Assert.True( statusResponse.IsSuccessStatusCode, $"Status check failed with status: {statusResponse.StatusCode}"); string statusText = await statusResponse.Content.ReadAsStringAsync(); this._outputHelper.WriteLine($"HITL orchestration status text: {statusText}"); JsonElement statusResult = JsonElement.Parse(statusText); // The orchestration should complete with a failed status due to timeout Assert.Equal("Failed", statusResult.GetProperty("runtimeStatus").GetString()); Assert.True(statusResult.TryGetProperty("failureDetails", out JsonElement failureDetailsElement)); Assert.True(failureDetailsElement.TryGetProperty("ErrorType", out JsonElement errorTypeElement)); Assert.Equal("System.TimeoutException", errorTypeElement.GetString()); Assert.True(failureDetailsElement.TryGetProperty("ErrorMessage", out JsonElement errorMessageElement)); Assert.StartsWith("Human approval timed out", errorMessageElement.GetString()); }); } [RetryFact(2, 5000)] public async Task LongRunningToolsSampleValidationAsync() { string samplePath = Path.Combine(s_samplesPath, "06_LongRunningTools"); await this.RunSampleTestAsync(samplePath, async (logs) => { // Test starting an agent that schedules a content generation orchestration const string Prompt = "Start a content generation workflow for the topic 'The Future of Artificial Intelligence'"; using HttpContent messageContent = new StringContent(Prompt, Encoding.UTF8, "text/plain"); Uri runAgentUri = new($"http://localhost:{AzureFunctionsPort}/api/agents/publisher/run"); this._outputHelper.WriteLine($"Starting agent tool orchestration via POST request to {runAgentUri}..."); using HttpResponseMessage startResponse = await s_sharedHttpClient.PostAsync(runAgentUri, messageContent); Assert.True( startResponse.IsSuccessStatusCode, $"Start agent request failed with status: {startResponse.StatusCode}"); string startResponseText = await startResponse.Content.ReadAsStringAsync(); this._outputHelper.WriteLine($"Agent response: {startResponseText}"); // The response should be deserializable as an AgentResponse object and have a valid session ID startResponse.Headers.TryGetValues("x-ms-thread-id", out IEnumerable? agentIdValues); string? sessionId = agentIdValues?.FirstOrDefault(); Assert.NotNull(sessionId); Assert.NotEmpty(sessionId); // Wait for the orchestration to report that it's waiting for human approval await this.WaitForConditionAsync( condition: () => { // For now, we have to rely on the logs to check for the "NOTIFICATION" message that gets generated by the activity function. // TODO: Synchronously prompt the agent for status lock (logs) { bool exists = logs.Any(log => log.Message.Contains("NOTIFICATION: Please review the following content for approval")); return Task.FromResult(exists); } }, message: "Orchestration is requesting human feedback", timeout: TimeSpan.FromSeconds(180)); // Approve the content Uri approvalUri = new($"{runAgentUri}?thread_id={sessionId}"); using HttpContent approvalContent = new StringContent("Approve the content", Encoding.UTF8, "text/plain"); using HttpResponseMessage approvalResponse = await s_sharedHttpClient.PostAsync(approvalUri, approvalContent); Assert.True(approvalResponse.IsSuccessStatusCode, $"Approve content request failed with status: {approvalResponse.StatusCode}"); // Wait for the publish notification to be logged await this.WaitForConditionAsync( condition: () => { lock (logs) { // TODO: Synchronously prompt the agent for status bool exists = logs.Any(log => log.Message.Contains("PUBLISHING: Content has been published successfully")); return Task.FromResult(exists); } }, message: "Content published notification is logged", timeout: TimeSpan.FromSeconds(180)); // Verify the final orchestration status by asking the agent for the status Uri statusUri = new($"{runAgentUri}?thread_id={sessionId}"); await this.WaitForConditionAsync( condition: async () => { this._outputHelper.WriteLine($"Checking status of orchestration at {statusUri}..."); using StringContent content = new("Get the status of the workflow", Encoding.UTF8, "text/plain"); using HttpResponseMessage statusResponse = await s_sharedHttpClient.PostAsync(statusUri, content); Assert.True( statusResponse.IsSuccessStatusCode, $"Status check failed with status: {statusResponse.StatusCode}"); string statusText = await statusResponse.Content.ReadAsStringAsync(); this._outputHelper.WriteLine($"Status text: {statusText}"); bool isCompleted = statusText.Contains("Completed", StringComparison.OrdinalIgnoreCase); bool hasContent = statusText.Contains( "The Future of Artificial Intelligence", StringComparison.OrdinalIgnoreCase); return isCompleted && hasContent; }, message: "Orchestration is completed", timeout: TimeSpan.FromSeconds(180)); }); } [RetryFact(2, 5000)] public async Task AgentAsMcpToolAsync() { string samplePath = Path.Combine(s_samplesPath, "07_AgentAsMcpTool"); await this.RunSampleTestAsync(samplePath, async (logs) => { IClientTransport clientTransport = new HttpClientTransport(new() { Endpoint = new Uri($"http://localhost:{AzureFunctionsPort}/runtime/webhooks/mcp") }); await using McpClient mcpClient = await McpClient.CreateAsync(clientTransport!); // Ensure the expected tools are present. IList tools = await mcpClient.ListToolsAsync(); Assert.Single(tools, t => t.Name == "StockAdvisor"); Assert.Single(tools, t => t.Name == "PlantAdvisor"); // Invoke the tools to verify they work as expected. string stockPriceResponse = await this.InvokeMcpToolAsync(mcpClient, "StockAdvisor", "MSFT ATH"); string plantSuggestionResponse = await this.InvokeMcpToolAsync(mcpClient, "PlantAdvisor", "Low light plant"); Assert.NotEmpty(stockPriceResponse); Assert.NotEmpty(plantSuggestionResponse); // Wait for up to 30 seconds to see if the agent responses are available in the logs await this.WaitForConditionAsync( condition: () => { lock (logs) { bool expectedLogsPresent = logs.Count(log => log.Message.Contains("Response:")) >= 2; return Task.FromResult(expectedLogsPresent); } }, message: "Agent response is available", timeout: TimeSpan.FromSeconds(30)); }); } [RetryFact(2, 5000)] public async Task ReliableStreamingSampleValidationAsync() { string samplePath = Path.Combine(s_samplesPath, "08_ReliableStreaming"); await this.RunSampleTestAsync(samplePath, async (logs) => { Uri createUri = new($"http://localhost:{AzureFunctionsPort}/api/agent/create"); this._outputHelper.WriteLine($"Starting reliable streaming agent via POST request to {createUri}..."); // Test the agent endpoint with a simple prompt const string RequestBody = "Plan a 3-day trip to Seattle. Include daily activities."; using HttpContent content = new StringContent(RequestBody, Encoding.UTF8, "text/plain"); using HttpRequestMessage request = new(HttpMethod.Post, createUri) { Content = content }; request.Headers.Add("Accept", "text/plain"); using HttpResponseMessage response = await s_sharedHttpClient.SendAsync( request, HttpCompletionOption.ResponseHeadersRead); // The response should be successful Assert.True(response.IsSuccessStatusCode, $"Agent request failed with status: {response.StatusCode}"); Assert.Equal("text/plain", response.Content.Headers.ContentType?.MediaType); // The response headers should include the conversation ID string? conversationId = response.Headers.GetValues("x-conversation-id")?.FirstOrDefault(); Assert.NotNull(conversationId); Assert.NotEmpty(conversationId); this._outputHelper.WriteLine($"Agent conversation ID: {conversationId}"); // Read the streamed response using Stream responseStream = await response.Content.ReadAsStreamAsync(); using StreamReader reader = new(responseStream); StringBuilder responseText = new(); char[] buffer = new char[1024]; int bytesRead; // Read for a reasonable amount of time to get some content using CancellationTokenSource readTimeout = new(TimeSpan.FromSeconds(30)); try { while (!readTimeout.Token.IsCancellationRequested) { bytesRead = await reader.ReadAsync(buffer, 0, buffer.Length); if (bytesRead == 0) { // Check if we've received enough content if (responseText.Length > 50) { break; } await Task.Delay(100, readTimeout.Token); continue; } responseText.Append(buffer, 0, bytesRead); if (responseText.Length > 200) { // We've received enough content to validate break; } } } catch (OperationCanceledException) { // Timeout is acceptable if we got some content } string responseContent = responseText.ToString(); Assert.True(responseContent.Length > 0, "Expected to receive some streamed content"); this._outputHelper.WriteLine($"Received {responseContent.Length} characters of streamed content"); // Test resumption by calling the stream endpoint Uri streamUri = new($"http://localhost:{AzureFunctionsPort}/api/agent/stream/{conversationId}"); this._outputHelper.WriteLine($"Testing stream resumption via GET request to {streamUri}..."); using HttpRequestMessage streamRequest = new(HttpMethod.Get, streamUri); streamRequest.Headers.Add("Accept", "text/plain"); using HttpResponseMessage streamResponse = await s_sharedHttpClient.SendAsync( streamRequest, HttpCompletionOption.ResponseHeadersRead); Assert.True(streamResponse.IsSuccessStatusCode, $"Stream request failed with status: {streamResponse.StatusCode}"); Assert.Equal("text/plain", streamResponse.Content.Headers.ContentType?.MediaType); // Verify the conversation ID header is present string? resumedConversationId = streamResponse.Headers.GetValues("x-conversation-id")?.FirstOrDefault(); Assert.Equal(conversationId, resumedConversationId); // Read some content from the resumed stream using Stream resumedStream = await streamResponse.Content.ReadAsStreamAsync(); using StreamReader resumedReader = new(resumedStream); StringBuilder resumedText = new(); using CancellationTokenSource resumedReadTimeout = new(TimeSpan.FromSeconds(10)); try { while (!resumedReadTimeout.Token.IsCancellationRequested) { bytesRead = await resumedReader.ReadAsync(buffer, 0, buffer.Length); if (bytesRead == 0) { if (resumedText.Length > 50) { break; } await Task.Delay(100, resumedReadTimeout.Token); continue; } resumedText.Append(buffer, 0, bytesRead); if (resumedText.Length > 100) { break; } } } catch (OperationCanceledException) { // Timeout is acceptable if we got some content } string resumedContent = resumedText.ToString(); Assert.True(resumedContent.Length > 0, "Expected to receive some content from resumed stream"); this._outputHelper.WriteLine($"Received {resumedContent.Length} characters from resumed stream"); }); } private async Task InvokeMcpToolAsync(McpClient mcpClient, string toolName, string query) { this._outputHelper.WriteLine($"Invoking MCP tool '{toolName}'..."); CallToolResult result = await mcpClient.CallToolAsync( toolName, arguments: new Dictionary { { "query", query } }); string toolCallResult = ((TextContentBlock)result.Content[0]).Text; this._outputHelper.WriteLine($"MCP tool '{toolName}' response: {toolCallResult}"); return toolCallResult; } private async Task TestSpamDetectionAsync(string emailId, string emailContent, bool expectedSpam) { object requestBody = new { email_id = emailId, email_content = emailContent }; string jsonContent = JsonSerializer.Serialize(requestBody); using HttpContent content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); Uri startUri = new($"http://localhost:{AzureFunctionsPort}/api/spamdetection/run"); this._outputHelper.WriteLine($"Starting spam detection orchestration via POST request to {startUri}..."); using HttpResponseMessage startResponse = await s_sharedHttpClient.PostAsync(startUri, content); Assert.True(startResponse.IsSuccessStatusCode, $"Start orchestration failed with status: {startResponse.StatusCode}"); string startResponseText = await startResponse.Content.ReadAsStringAsync(); JsonElement startResult = JsonElement.Parse(startResponseText); Assert.True(startResult.TryGetProperty("statusQueryGetUri", out JsonElement statusUriElement)); Uri statusUri = new(statusUriElement.GetString()!); // Wait for orchestration to complete await this.WaitForOrchestrationCompletionAsync(statusUri); // Verify the final result using HttpResponseMessage statusResponse = await s_sharedHttpClient.GetAsync(statusUri); Assert.True(statusResponse.IsSuccessStatusCode, $"Status check failed with status: {statusResponse.StatusCode}"); string statusText = await statusResponse.Content.ReadAsStringAsync(); JsonElement statusResult = JsonElement.Parse(statusText); Assert.Equal("Completed", statusResult.GetProperty("runtimeStatus").GetString()); Assert.True(statusResult.TryGetProperty("output", out JsonElement outputElement)); string output = outputElement.GetString()!; Assert.NotEmpty(output); if (expectedSpam) { Assert.Contains("spam", output, StringComparison.OrdinalIgnoreCase); } else { Assert.Contains("sent", output, StringComparison.OrdinalIgnoreCase); } } private async Task StartSharedInfrastructureAsync() { // Start Azurite if it's not already running if (!await this.IsAzuriteRunningAsync()) { await this.StartDockerContainerAsync( containerName: "azurite", image: "mcr.microsoft.com/azure-storage/azurite", ports: ["-p", "10000:10000", "-p", "10001:10001", "-p", "10002:10002"]); // Wait for Azurite await this.WaitForConditionAsync(this.IsAzuriteRunningAsync, "Azurite is running", TimeSpan.FromSeconds(30)); } // Start DTS emulator if it's not already running if (!await this.IsDtsEmulatorRunningAsync()) { await this.StartDockerContainerAsync( containerName: "dts-emulator", image: "mcr.microsoft.com/dts/dts-emulator:latest", ports: ["-p", "8080:8080", "-p", "8082:8082"]); // Wait for DTS emulator await this.WaitForConditionAsync( condition: this.IsDtsEmulatorRunningAsync, message: "DTS emulator is running", timeout: TimeSpan.FromSeconds(30)); } // Start Redis if it's not already running if (!await this.IsRedisRunningAsync()) { await this.StartDockerContainerAsync( containerName: "redis", image: "redis:latest", ports: ["-p", "6379:6379"]); // Wait for Redis await this.WaitForConditionAsync( condition: this.IsRedisRunningAsync, message: "Redis is running", timeout: TimeSpan.FromSeconds(30)); } } private async Task IsAzuriteRunningAsync() { this._outputHelper.WriteLine( $"Checking if Azurite is running at http://localhost:{AzuritePort}/devstoreaccount1..."); try { using CancellationTokenSource timeoutCts = new(TimeSpan.FromSeconds(30)); // Example output when pinging Azurite: // $ curl -i http://localhost:10000/devstoreaccount1?comp=list // HTTP/1.1 403 Server failed to authenticate the request. // Server: Azurite-Blob/3.34.0 // x-ms-error-code: AuthorizationFailure // x-ms-request-id: 6cd21522-bb0f-40f6-962c-fa174f17aa30 // content-type: application/xml // Date: Mon, 20 Oct 2025 23:52:02 GMT // Connection: keep-alive // Keep-Alive: timeout=5 // Transfer-Encoding: chunked using HttpResponseMessage response = await s_sharedHttpClient.GetAsync( requestUri: new Uri($"http://localhost:{AzuritePort}/devstoreaccount1?comp=list"), cancellationToken: timeoutCts.Token); if (response.Headers.TryGetValues( "Server", out IEnumerable? serverValues) && serverValues.Any(s => s.StartsWith("Azurite", StringComparison.OrdinalIgnoreCase))) { this._outputHelper.WriteLine($"Azurite is running, server: {string.Join(", ", serverValues)}"); return true; } this._outputHelper.WriteLine($"Azurite is not running. Status code: {response.StatusCode}"); return false; } catch (HttpRequestException ex) { this._outputHelper.WriteLine($"Azurite is not running: {ex.Message}"); return false; } } private async Task IsDtsEmulatorRunningAsync() { this._outputHelper.WriteLine($"Checking if DTS emulator is running at http://localhost:{DtsPort}/healthz..."); // DTS emulator doesn't support HTTP/1.1, so we need to use HTTP/2.0 using HttpClient http2Client = new() { DefaultRequestVersion = new Version(2, 0), DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact }; try { using CancellationTokenSource timeoutCts = new(TimeSpan.FromSeconds(30)); using HttpResponseMessage response = await http2Client.GetAsync(new Uri($"http://localhost:{DtsPort}/healthz"), timeoutCts.Token); if (response.Content.Headers.ContentLength > 0) { string content = await response.Content.ReadAsStringAsync(timeoutCts.Token); this._outputHelper.WriteLine($"DTS emulator health check response: {content}"); } if (response.IsSuccessStatusCode) { this._outputHelper.WriteLine("DTS emulator is running"); return true; } this._outputHelper.WriteLine($"DTS emulator is not running. Status code: {response.StatusCode}"); return false; } catch (HttpRequestException ex) { this._outputHelper.WriteLine($"DTS emulator is not running: {ex.Message}"); return false; } } private async Task IsRedisRunningAsync() { this._outputHelper.WriteLine($"Checking if Redis is running at localhost:{RedisPort}..."); try { using CancellationTokenSource timeoutCts = new(TimeSpan.FromSeconds(30)); ProcessStartInfo startInfo = new() { FileName = "docker", Arguments = "exec redis redis-cli ping", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; using Process process = new() { StartInfo = startInfo }; if (!process.Start()) { this._outputHelper.WriteLine("Failed to start docker exec command"); return false; } string output = await process.StandardOutput.ReadToEndAsync(timeoutCts.Token); await process.WaitForExitAsync(timeoutCts.Token); if (process.ExitCode == 0 && output.Contains("PONG", StringComparison.OrdinalIgnoreCase)) { this._outputHelper.WriteLine("Redis is running"); return true; } this._outputHelper.WriteLine($"Redis is not running. Exit code: {process.ExitCode}, Output: {output}"); return false; } catch (Exception ex) { this._outputHelper.WriteLine($"Redis is not running: {ex.Message}"); return false; } } private async Task StartDockerContainerAsync(string containerName, string image, string[] ports) { // Stop existing container if it exists await this.RunCommandAsync("docker", ["stop", containerName]); await this.RunCommandAsync("docker", ["rm", containerName]); // Start new container List args = ["run", "-d", "--name", containerName]; args.AddRange(ports); args.Add(image); this._outputHelper.WriteLine( $"Starting new container: {containerName} with image: {image} and ports: {string.Join(", ", ports)}"); await this.RunCommandAsync("docker", args.ToArray()); this._outputHelper.WriteLine($"Container started: {containerName}"); } private async Task WaitForConditionAsync(Func> condition, string message, TimeSpan timeout) { this._outputHelper.WriteLine($"Waiting for '{message}'..."); using CancellationTokenSource cancellationTokenSource = new(timeout); while (true) { if (await condition()) { return; } try { await Task.Delay(TimeSpan.FromSeconds(1), cancellationTokenSource.Token); } catch (OperationCanceledException) when (cancellationTokenSource.IsCancellationRequested) { throw new TimeoutException($"Timeout waiting for '{message}'"); } } } private async Task RunSampleTestAsync(string samplePath, Func, Task> testAction) { // Build the sample project first (it may not have been built as part of the solution) await AzureFunctionsTestHelper.BuildSampleAsync( samplePath, $"-f {s_dotnetTargetFramework} -c {BuildConfiguration}", this._outputHelper); // Start the Azure Functions app List logsContainer = []; using Process funcProcess = this.StartFunctionApp(samplePath, logsContainer); try { // Wait for the app to be ready await AzureFunctionsTestHelper.WaitForFunctionsReadyAsync( funcProcess, AzureFunctionsPort, s_sharedHttpClient, this._outputHelper, s_functionsReadyTimeout, samplePath); // Run the test await testAction(logsContainer); } finally { await this.StopProcessAsync(funcProcess); } } private sealed record OutputLog(DateTime Timestamp, LogLevel Level, string Message); private Process StartFunctionApp(string samplePath, List logs) { ProcessStartInfo startInfo = new() { FileName = "dotnet", Arguments = $"run --no-build -f {s_dotnetTargetFramework} -c {BuildConfiguration} --port {AzureFunctionsPort}", WorkingDirectory = samplePath, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, }; string openAiEndpoint = s_configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("The required AZURE_OPENAI_ENDPOINT env variable is not set."); string openAiDeployment = s_configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("The required AZURE_OPENAI_DEPLOYMENT_NAME env variable is not set."); // Set required environment variables for the function app (see local.settings.json for required settings) startInfo.EnvironmentVariables["AZURE_OPENAI_ENDPOINT"] = openAiEndpoint; startInfo.EnvironmentVariables["AZURE_OPENAI_DEPLOYMENT_NAME"] = openAiDeployment; startInfo.EnvironmentVariables["DURABLE_TASK_SCHEDULER_CONNECTION_STRING"] = $"Endpoint=http://localhost:{DtsPort};TaskHub=default;Authentication=None"; startInfo.EnvironmentVariables["AzureWebJobsStorage"] = "UseDevelopmentStorage=true"; startInfo.EnvironmentVariables["REDIS_CONNECTION_STRING"] = $"localhost:{RedisPort}"; Process process = new() { StartInfo = startInfo }; // Capture the output and error streams process.ErrorDataReceived += (sender, e) => { if (e.Data != null) { this._outputHelper.WriteLine($"[{startInfo.FileName}(err)]: {e.Data}"); lock (logs) { logs.Add(new OutputLog(DateTime.Now, LogLevel.Error, e.Data)); } } }; process.OutputDataReceived += (sender, e) => { if (e.Data != null) { this._outputHelper.WriteLine($"[{startInfo.FileName}(out)]: {e.Data}"); lock (logs) { logs.Add(new OutputLog(DateTime.Now, LogLevel.Information, e.Data)); } } }; if (!process.Start()) { throw new InvalidOperationException("Failed to start the function app"); } process.BeginErrorReadLine(); process.BeginOutputReadLine(); return process; } private async Task WaitForOrchestrationCompletionAsync(Uri statusUri) { using CancellationTokenSource timeoutCts = new(s_orchestrationTimeout); while (true) { try { using HttpResponseMessage response = await s_sharedHttpClient.GetAsync( statusUri, timeoutCts.Token); if (response.IsSuccessStatusCode) { string responseText = await response.Content.ReadAsStringAsync(timeoutCts.Token); JsonElement result = JsonElement.Parse(responseText); if (result.TryGetProperty("runtimeStatus", out JsonElement statusElement) && statusElement.GetString() is "Completed" or "Failed" or "Terminated") { return; } } } catch (Exception ex) when (!timeoutCts.Token.IsCancellationRequested) { // Ignore errors and retry this._outputHelper.WriteLine($"Error waiting for orchestration completion: {ex}"); } await Task.Delay(TimeSpan.FromSeconds(1), timeoutCts.Token); } } private async Task RunCommandAsync(string command, string[] args) { await this.RunCommandAsync(command, workingDirectory: null, args: args); } private async Task RunCommandAsync(string command, string? workingDirectory, string[] args) { ProcessStartInfo startInfo = new() { FileName = command, Arguments = string.Join(" ", args), WorkingDirectory = workingDirectory, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; this._outputHelper.WriteLine($"Running command: {command} {string.Join(" ", args)}"); using Process process = new() { StartInfo = startInfo }; process.ErrorDataReceived += (sender, e) => this._outputHelper.WriteLine($"[{command}(err)]: {e.Data}"); process.OutputDataReceived += (sender, e) => this._outputHelper.WriteLine($"[{command}(out)]: {e.Data}"); if (!process.Start()) { throw new InvalidOperationException("Failed to start the command"); } process.BeginErrorReadLine(); process.BeginOutputReadLine(); using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromMinutes(1)); await process.WaitForExitAsync(cancellationTokenSource.Token); this._outputHelper.WriteLine($"Command completed with exit code: {process.ExitCode}"); } private async Task StopProcessAsync(Process process) { try { if (!process.HasExited) { this._outputHelper.WriteLine($"Killing process {process.ProcessName}#{process.Id}"); process.Kill(entireProcessTree: true); using CancellationTokenSource timeoutCts = new(TimeSpan.FromSeconds(10)); await process.WaitForExitAsync(timeoutCts.Token); this._outputHelper.WriteLine($"Process exited: {process.Id}"); } } catch (Exception ex) { this._outputHelper.WriteLine($"Failed to stop process: {ex.Message}"); } } private static string GetTargetFramework() { // Get the target framework by looking at the path of the current file. It should be something like /path/to/project/bin/Debug/net8.0/... string filePath = new Uri(typeof(SamplesValidation).Assembly.Location).LocalPath; string directory = Path.GetDirectoryName(filePath)!; string tfm = Path.GetFileName(directory); if (tfm.StartsWith("net", StringComparison.OrdinalIgnoreCase)) { return tfm; } throw new InvalidOperationException($"Unable to find target framework in path: {filePath}"); } }