From b2384a905674ef66b9761d982fba75bd445c9e1e Mon Sep 17 00:00:00 2001 From: octo-patch Date: Tue, 28 Apr 2026 10:29:58 +0800 Subject: [PATCH 1/4] fix: address static analysis bugs in audio format, text search, and KernelProcess - Replace duplicate ChatOutputAudioFormat.Wav check with Aac in GetAudioOutputMimeType; update error message to include 'aac' - Remove dead LINQ variable in TextSearchStore.GetTextSearchResultsAsync and reuse it in the return statement to avoid duplicate expression - Assign the threads constructor parameter to KernelProcess.Threads property so it is not silently discarded (fixes #13922) Co-Authored-By: Octopus --- .../Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs | 6 +++--- .../src/Experimental/Process.Abstractions/KernelProcess.cs | 4 ++++ .../Data/TextSearchStore/TextSearchStore.cs | 7 +------ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs index 3387601ed189..1aa391d1acac 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs @@ -997,9 +997,9 @@ private OpenAIChatMessageContent CreateChatMessageContent(OAIChat.ChatCompletion return "audio/opus"; } - if (audioOptions.OutputAudioFormat == ChatOutputAudioFormat.Wav) + if (audioOptions.OutputAudioFormat == ChatOutputAudioFormat.Aac) { - return "audio/wav"; + return "audio/aac"; } if (audioOptions.OutputAudioFormat == ChatOutputAudioFormat.Flac) @@ -1012,7 +1012,7 @@ private OpenAIChatMessageContent CreateChatMessageContent(OAIChat.ChatCompletion return "audio/pcm16"; } - throw new NotSupportedException($"Unsupported audio output format '{audioOptions.OutputAudioFormat}'. Supported formats are 'wav', 'mp3', 'opus', 'flac' and 'pcm16'."); + throw new NotSupportedException($"Unsupported audio output format '{audioOptions.OutputAudioFormat}'. Supported formats are 'wav', 'mp3', 'opus', 'aac', 'flac' and 'pcm16'."); } private OpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs index d35c29ad6a78..9c73440e4762 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs @@ -50,5 +50,9 @@ public KernelProcess(KernelProcessState state, IList step Verify.NotNullOrWhiteSpace(state.Name); this.Steps = [.. steps]; + if (threads is not null) + { + this.Threads = threads; + } } } diff --git a/dotnet/src/SemanticKernel.Core/Data/TextSearchStore/TextSearchStore.cs b/dotnet/src/SemanticKernel.Core/Data/TextSearchStore/TextSearchStore.cs index d58ba26c6555..25e7cd9a8766 100644 --- a/dotnet/src/SemanticKernel.Core/Data/TextSearchStore/TextSearchStore.cs +++ b/dotnet/src/SemanticKernel.Core/Data/TextSearchStore/TextSearchStore.cs @@ -202,12 +202,7 @@ public async Task> GetTextSearchResultsAsy var searchResult = await this.SearchInternalAsync(query, searchOptions, cancellationToken).ConfigureAwait(false); var results = searchResult.Select(x => new TextSearchResult(x.Text ?? string.Empty) { Name = x.SourceName, Link = x.SourceLink }); - return new(searchResult.Select(x => - new TextSearchResult(x.Text ?? string.Empty) - { - Name = x.SourceName, - Link = x.SourceLink - }).ToAsyncEnumerable()); + return new(results.ToAsyncEnumerable()); } /// From 09012e2360c9dc82cde6e2692885a74ced06cc54 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:57:25 +0100 Subject: [PATCH 2/4] .Net: test: add unit tests for static analysis fixes (#13922) Adds focused unit tests covering the three bugs fixed in PR #13925: - OpenAIChatCompletionServiceTests.ItMapsAudioOutputFormatToCorrectMimeTypeAsync: Theory across all six ChatOutputAudioFormat values (Wav, Aac, Mp3, Opus, Flac, Pcm16) asserting AudioContent.MimeType. The Aac row fails on the pre-fix code because GetAudioOutputMimeType had a duplicate Wav branch shadowing the missing Aac mapping. - KernelProcessTests (new file): two facts validating that the KernelProcess constructor assigns the supplied threads dictionary to the Threads property and defaults to an empty dictionary when null. - TextSearchStoreTests.GetTextSearchResultsAsyncReturnsTextSearchResults: fills a coverage gap on GetTextSearchResultsAsync, asserting Value, Name and Link projection from the underlying TextRagStorageDocument. --- .../OpenAIChatCompletionServiceTests.cs | 60 +++++++++++++++++++ .../Process.UnitTests/KernelProcessTests.cs | 60 +++++++++++++++++++ .../Data/TextSearchStoreTests.cs | 31 ++++++++++ 3 files changed, 151 insertions(+) create mode 100644 dotnet/src/Experimental/Process.UnitTests/KernelProcessTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs index 397c9bb0e39d..43ae9a2b76a1 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs @@ -1936,6 +1936,16 @@ public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext co { "{\"voice\":\"echo\",\"format\":\"opus\"}", "{\"voice\":\"echo\",\"format\":\"opus\"}" }, }; + public static TheoryData AudioMimeTypeMappingData => new() + { + { ChatOutputAudioFormat.Wav, "audio/wav" }, + { ChatOutputAudioFormat.Aac, "audio/aac" }, + { ChatOutputAudioFormat.Mp3, "audio/mp3" }, + { ChatOutputAudioFormat.Opus, "audio/opus" }, + { ChatOutputAudioFormat.Flac, "audio/flac" }, + { ChatOutputAudioFormat.Pcm16, "audio/pcm16" }, + }; + #pragma warning disable CS8618, CA1812 private sealed class MathReasoning { @@ -2146,6 +2156,56 @@ public async Task ItHandlesAudioContentWithMetadataInResponseAsync() // The ExpiresAt value is converted to a DateTime object, so we can't directly compare it to the Unix timestamp } + [Theory] + [MemberData(nameof(AudioMimeTypeMappingData))] + public async Task ItMapsAudioOutputFormatToCorrectMimeTypeAsync(ChatOutputAudioFormat format, string expectedMimeType) + { + // Arrange + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-4o", apiKey: "NOKEY", httpClient: this._httpClient); + + var responseJson = """ + { + "model": "gpt-4o", + "choices": [ + { + "message": { + "role": "assistant", + "content": "This is the text response.", + "audio": { + "data": "AQIDBA==" + } + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 20, + "total_tokens": 30 + } + } + """; + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { Content = new StringContent(responseJson) }; + + var settings = new OpenAIPromptExecutionSettings + { + Modalities = ChatResponseModalities.Text | ChatResponseModalities.Audio, + Audio = new ChatAudioOptions(ChatOutputAudioVoice.Alloy, format) + }; + + // Act + var result = await chatCompletion.GetChatMessageContentAsync(this._chatHistoryForTest, settings); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Items.Count); + var audioContent = result.Items[1] as AudioContent; + Assert.NotNull(audioContent); + Assert.Equal(expectedMimeType, audioContent.MimeType); + } + [Fact] public async Task GetChatMessageContentsThrowsExceptionWithEmptyBinaryContentAsync() { diff --git a/dotnet/src/Experimental/Process.UnitTests/KernelProcessTests.cs b/dotnet/src/Experimental/Process.UnitTests/KernelProcessTests.cs new file mode 100644 index 000000000000..11bc7431683c --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/KernelProcessTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.SemanticKernel.Process.UnitTests; + +/// +/// Unit tests for . +/// +public class KernelProcessTests +{ + /// + /// Verifies that the constructor assigns the supplied + /// threads dictionary to the property + /// instead of silently discarding it. + /// + [Fact] + public void Constructor_AssignsThreadsParameter_WhenProvided() + { + // Arrange + var state = new KernelProcessState(name: "TestProcess", version: "v1", id: "p1"); + var steps = new List(); + var threads = new Dictionary + { + ["main"] = new KernelProcessAgentThread { ThreadName = "main", ThreadId = "t-1" }, + ["aux"] = new KernelProcessAgentThread { ThreadName = "aux", ThreadId = "t-2" }, + }; + + // Act + var process = new KernelProcess(state, steps, edges: null, threads: threads); + + // Assert + Assert.Equal(2, process.Threads.Count); + Assert.True(process.Threads.ContainsKey("main")); + Assert.True(process.Threads.ContainsKey("aux")); + Assert.Equal("t-1", process.Threads["main"].ThreadId); + Assert.Equal("t-2", process.Threads["aux"].ThreadId); + } + + /// + /// Verifies that the constructor leaves + /// as an empty dictionary when the + /// threads argument is null. + /// + [Fact] + public void Constructor_DefaultsThreadsToEmptyDictionary_WhenNull() + { + // Arrange + var state = new KernelProcessState(name: "TestProcess", version: "v1", id: "p2"); + var steps = new List(); + + // Act + var process = new KernelProcess(state, steps, edges: null, threads: null); + + // Assert + Assert.NotNull(process.Threads); + Assert.Empty(process.Threads); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Data/TextSearchStoreTests.cs b/dotnet/src/SemanticKernel.UnitTests/Data/TextSearchStoreTests.cs index a2a39a35ea7b..49543725399c 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Data/TextSearchStoreTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Data/TextSearchStoreTests.cs @@ -245,6 +245,37 @@ public async Task SearchAsyncReturnsSearchResults() Assert.Equal("Sample text", actualResultsList[0]); } + [Fact] + public async Task GetTextSearchResultsAsyncReturnsTextSearchResults() + { + // Arrange + var mockResults = new List.TextRagStorageDocument>> + { + new(new TextSearchStore.TextRagStorageDocument + { + Text = "Sample text", + SourceName = "src-name", + SourceLink = "src-link", + }, 0.9f) + }; + + this._recordCollectionMock + .Setup(r => r.SearchAsync("query", 3, It.IsAny.TextRagStorageDocument>>(), It.IsAny())) + .Returns(mockResults.ToAsyncEnumerable()); + + using var store = new TextSearchStore(this._vectorStoreMock.Object, "testCollection", 128); + + // Act + var actualResults = await store.GetTextSearchResultsAsync("query"); + + // Assert + var actualResultsList = await actualResults.Results.ToListAsync(); + Assert.Single(actualResultsList); + Assert.Equal("Sample text", actualResultsList[0].Value); + Assert.Equal("src-name", actualResultsList[0].Name); + Assert.Equal("src-link", actualResultsList[0].Link); + } + [Fact] public async Task SearchAsyncWithHybridReturnsSearchResults() { From 41d9c1ad5d95328bbb931271987dd966a4612696 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:26:08 +0100 Subject: [PATCH 3/4] .Net: refactor: defensively copy threads dictionary in KernelProcess ctor Mirrors the existing 'Steps = [.. steps]' pattern so the Threads property (typed as IReadOnlyDictionary) cannot be mutated post-construction via the caller's original dictionary reference. Addresses Copilot review feedback on PR #13925. --- dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs index 9c73440e4762..80901b5340ce 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.SemanticKernel.Process.Internal; using Microsoft.SemanticKernel.Process.Models; @@ -52,7 +53,7 @@ public KernelProcess(KernelProcessState state, IList step this.Steps = [.. steps]; if (threads is not null) { - this.Threads = threads; + this.Threads = threads.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } } } From 39766a8e695fc844c1ed817c36420987e6f362a6 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:43:09 +0100 Subject: [PATCH 4/4] .Net: style: fix IDE0055 in PromptExecutionSettingsExtensions Applied 'dotnet format' to PromptExecutionSettingsExtensions.cs to clear pre-existing IDE0055 indentation warnings on the else/if cascade that were blocking check-format on this PR. --- .../AI/PromptExecutionSettingsExtensions.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettingsExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettingsExtensions.cs index 59274f537261..e756249b225d 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettingsExtensions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettingsExtensions.cs @@ -176,16 +176,16 @@ public static class PromptExecutionSettingsExtensions options.AllowMultipleToolCalls = autoChoiceBehavior.Options?.AllowParallelCalls; } else - if (settings.FunctionChoiceBehavior is NoneFunctionChoiceBehavior noneFunctionChoiceBehavior) - { - options.ToolMode = ChatToolMode.None; - } - else - if (settings.FunctionChoiceBehavior is RequiredFunctionChoiceBehavior requiredFunctionChoiceBehavior) - { - options.ToolMode = ChatToolMode.RequireAny; - options.AllowMultipleToolCalls = requiredFunctionChoiceBehavior.Options?.AllowParallelCalls; - } + if (settings.FunctionChoiceBehavior is NoneFunctionChoiceBehavior noneFunctionChoiceBehavior) + { + options.ToolMode = ChatToolMode.None; + } + else + if (settings.FunctionChoiceBehavior is RequiredFunctionChoiceBehavior requiredFunctionChoiceBehavior) + { + options.ToolMode = ChatToolMode.RequireAny; + options.AllowMultipleToolCalls = requiredFunctionChoiceBehavior.Options?.AllowParallelCalls; + } options.Tools = []; foreach (var function in functions)