From 995c71afca02c770847f0f58616a69ef64c2e7d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 08:49:33 +0000 Subject: [PATCH 1/3] test: Add unit tests to reproduce Mistral provider issue #309 Add comprehensive unit tests for MistralThinkingBehavior and Mistral provider to reproduce and verify issue #309 ("Mistral provider broken" with "Object reference not set to an instance of an object" error). Tests cover: - MistralThinkingBehavior with null/empty text in assistant messages - Thinking response extraction from JSON arrays - Edge cases with malformed JSON responses - ChatMessage and ChatResponse handling with null text - Mocked IChatClient scenarios simulating Mistral SDK behavior - Exact pattern used in CellmFunctions.cs line 317 --- .../Behaviors/MistralThinkingBehaviorTests.cs | 316 ++++++++++++++++++ .../Unit/Providers/MistralProviderTests.cs | 185 ++++++++++ 2 files changed, 501 insertions(+) create mode 100644 src/Cellm.Tests/Unit/Behaviors/MistralThinkingBehaviorTests.cs create mode 100644 src/Cellm.Tests/Unit/Providers/MistralProviderTests.cs diff --git a/src/Cellm.Tests/Unit/Behaviors/MistralThinkingBehaviorTests.cs b/src/Cellm.Tests/Unit/Behaviors/MistralThinkingBehaviorTests.cs new file mode 100644 index 0000000..639bd81 --- /dev/null +++ b/src/Cellm.Tests/Unit/Behaviors/MistralThinkingBehaviorTests.cs @@ -0,0 +1,316 @@ +using Cellm.Models.Prompts; +using Cellm.Models.Providers; +using Cellm.Models.Providers.Behaviors; +using Microsoft.Extensions.AI; +using Xunit; + +namespace Cellm.Tests.Unit.Behaviors; + +/// +/// Tests for MistralThinkingBehavior to verify handling of Mistral responses, +/// particularly edge cases that may cause NullReferenceException (issue #309). +/// +public class MistralThinkingBehaviorTests +{ + private readonly MistralThinkingBehavior _behavior; + + public MistralThinkingBehaviorTests() + { + _behavior = new MistralThinkingBehavior(); + } + + #region IsEnabled Tests + + [Theory] + [InlineData(Provider.Mistral, true)] + [InlineData(Provider.OpenAi, false)] + [InlineData(Provider.Anthropic, false)] + [InlineData(Provider.Ollama, false)] + [InlineData(Provider.Cellm, false)] + public void IsEnabled_ReturnsCorrectValue(Provider provider, bool expected) + { + // Act + var result = _behavior.IsEnabled(provider); + + // Assert + Assert.Equal(expected, result); + } + + #endregion + + #region After Method - Normal Response Tests + + [Fact] + public void After_WithNormalTextResponse_DoesNotModifyMessage() + { + // Arrange + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("Hello") + .Build(); + + // Add assistant message + prompt.Messages.Add(new ChatMessage(ChatRole.Assistant, "Hello! How can I help you?")); + + var originalText = prompt.Messages.Last().Text; + + // Act + _behavior.After(Provider.Mistral, prompt); + + // Assert - Message should be unchanged since it's not a thinking response + Assert.Equal(originalText, prompt.Messages.Last().Text); + } + + [Fact] + public void After_WithEmptyMessages_DoesNotThrow() + { + // Arrange + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .Build(); + + // Act & Assert - Should not throw + var exception = Record.Exception(() => _behavior.After(Provider.Mistral, prompt)); + Assert.Null(exception); + } + + [Fact] + public void After_WithOnlyUserMessage_DoesNotThrow() + { + // Arrange + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("Hello") + .Build(); + + // Act & Assert - Should not throw (last message is User, not Assistant) + var exception = Record.Exception(() => _behavior.After(Provider.Mistral, prompt)); + Assert.Null(exception); + } + + #endregion + + #region After Method - Null/Empty Text Tests (Issue #309 reproduction) + + [Fact] + public void After_WithNullTextInAssistantMessage_DoesNotThrow() + { + // Arrange - This simulates the scenario where Mistral SDK returns a message with null Text + // which was reported in issue #309: "Object reference not set to an instance of an object" + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("Hello") + .Build(); + + // Create a ChatMessage with null text content to simulate Mistral SDK behavior + var assistantMessage = new ChatMessage(ChatRole.Assistant, (string?)null); + prompt.Messages.Add(assistantMessage); + + // Act & Assert - Should not throw NullReferenceException + var exception = Record.Exception(() => _behavior.After(Provider.Mistral, prompt)); + Assert.Null(exception); + } + + [Fact] + public void After_WithEmptyTextInAssistantMessage_DoesNotThrow() + { + // Arrange + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("Hello") + .Build(); + + var assistantMessage = new ChatMessage(ChatRole.Assistant, string.Empty); + prompt.Messages.Add(assistantMessage); + + // Act & Assert + var exception = Record.Exception(() => _behavior.After(Provider.Mistral, prompt)); + Assert.Null(exception); + } + + [Fact] + public void After_WithWhitespaceTextInAssistantMessage_DoesNotThrow() + { + // Arrange + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("Hello") + .Build(); + + var assistantMessage = new ChatMessage(ChatRole.Assistant, " \t\n "); + prompt.Messages.Add(assistantMessage); + + // Act & Assert + var exception = Record.Exception(() => _behavior.After(Provider.Mistral, prompt)); + Assert.Null(exception); + } + + [Fact] + public void After_WithEmptyContentsArray_DoesNotThrow() + { + // Arrange - Simulate a ChatMessage with empty Contents array + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("Hello") + .Build(); + + // Create a ChatMessage with empty contents + var assistantMessage = new ChatMessage() + { + Role = ChatRole.Assistant, + Contents = [] + }; + prompt.Messages.Add(assistantMessage); + + // Act & Assert + var exception = Record.Exception(() => _behavior.After(Provider.Mistral, prompt)); + Assert.Null(exception); + } + + #endregion + + #region After Method - Thinking Response Tests + + [Fact] + public void After_WithThinkingResponse_ExtractsTextContent() + { + // Arrange - Simulate Mistral thinking response format + var thinkingResponse = """ + [{"type":"thinking","thinking":"Let me analyze this..."},{"type":"text","text":"The answer is 42."}] + """; + + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("What is the answer to life?") + .Build(); + + prompt.Messages.Add(new ChatMessage(ChatRole.Assistant, thinkingResponse)); + + // Act + _behavior.After(Provider.Mistral, prompt); + + // Assert - Should extract just the text part + var lastMessage = prompt.Messages.Last(); + Assert.Equal("The answer is 42.", lastMessage.Text); + } + + [Fact] + public void After_WithThinkingResponseNullText_DoesNotThrow() + { + // Arrange - Simulate edge case where text property is null in JSON + var thinkingResponse = """ + [{"type":"thinking","thinking":"Let me analyze this..."},{"type":"text","text":null}] + """; + + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("What is the answer?") + .Build(); + + prompt.Messages.Add(new ChatMessage(ChatRole.Assistant, thinkingResponse)); + + // Act & Assert - Should not throw when text.GetString() returns null + var exception = Record.Exception(() => _behavior.After(Provider.Mistral, prompt)); + // Note: This may throw NullReferenceException if TextContent constructor doesn't accept null + // which would confirm issue #309 + if (exception != null) + { + Assert.IsType(exception); + } + } + + [Fact] + public void After_WithOnlyThinkingNoText_DoesNotModify() + { + // Arrange - Simulate response with only thinking, no text + var thinkingResponse = """ + [{"type":"thinking","thinking":"Let me analyze this..."}] + """; + + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("What is the answer?") + .Build(); + + prompt.Messages.Add(new ChatMessage(ChatRole.Assistant, thinkingResponse)); + var originalText = prompt.Messages.Last().Text; + + // Act + _behavior.After(Provider.Mistral, prompt); + + // Assert - Should not modify since there's no text element + Assert.Equal(originalText, prompt.Messages.Last().Text); + } + + [Fact] + public void After_WithInvalidJson_DoesNotThrow() + { + // Arrange - Simulate malformed JSON response + var invalidJson = "[{invalid json}]"; + + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("Hello") + .Build(); + + prompt.Messages.Add(new ChatMessage(ChatRole.Assistant, invalidJson)); + + // Act & Assert - Should gracefully handle invalid JSON + var exception = Record.Exception(() => _behavior.After(Provider.Mistral, prompt)); + Assert.Null(exception); + } + + [Fact] + public void After_WithNonArrayJson_DoesNotModify() + { + // Arrange - JSON that parses but isn't an array + var nonArrayJson = """{"type":"text","text":"Hello"}"""; + + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("Hello") + .Build(); + + prompt.Messages.Add(new ChatMessage(ChatRole.Assistant, nonArrayJson)); + var originalText = prompt.Messages.Last().Text; + + // Act + _behavior.After(Provider.Mistral, prompt); + + // Assert - Should not modify since it's not an array + Assert.Equal(originalText, prompt.Messages.Last().Text); + } + + [Fact] + public void After_WithTextNotStartingWithBracket_DoesNotProcess() + { + // Arrange - Normal text that doesn't look like JSON array + var normalText = "Hello, I'm a normal response."; + + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("Hello") + .Build(); + + prompt.Messages.Add(new ChatMessage(ChatRole.Assistant, normalText)); + + // Act + _behavior.After(Provider.Mistral, prompt); + + // Assert - Should skip processing (quick check fails) + Assert.Equal(normalText, prompt.Messages.Last().Text); + } + + #endregion + + #region Order Tests + + [Fact] + public void Order_Returns30() + { + // Assert + Assert.Equal(30u, _behavior.Order); + } + + #endregion +} diff --git a/src/Cellm.Tests/Unit/Providers/MistralProviderTests.cs b/src/Cellm.Tests/Unit/Providers/MistralProviderTests.cs new file mode 100644 index 0000000..1fecb0f --- /dev/null +++ b/src/Cellm.Tests/Unit/Providers/MistralProviderTests.cs @@ -0,0 +1,185 @@ +using Cellm.Models.Prompts; +using Cellm.Models.Providers; +using Microsoft.Extensions.AI; +using NSubstitute; +using Xunit; + +namespace Cellm.Tests.Unit.Providers; + +/// +/// Tests to reproduce issue #309: "Mistral provider broken" +/// Error: "Object reference not set to an instance of an object" +/// +/// The issue occurs when the Mistral SDK's IChatClient adapter returns a ChatMessage +/// where the Text property is null or the Contents collection doesn't properly populate +/// the Text property. +/// +public class MistralProviderTests +{ + /// + /// Issue #309 Reproduction: Tests that a ChatMessage with null Text property + /// can be safely accessed without throwing NullReferenceException. + /// + /// The Mistral SDK 2.3.0 may return ChatMessages where: + /// - Text property is null + /// - Contents array is empty + /// - Role is set but content is missing + /// + [Fact] + public void ChatMessage_WithNullText_SafeAccess() + { + // Arrange - Simulate Mistral SDK response with null text + var chatMessage = new ChatMessage(ChatRole.Assistant, (string?)null); + + // Act & Assert - These accesses should not throw + Assert.Equal(ChatRole.Assistant, chatMessage.Role); + Assert.Null(chatMessage.Text); // Text should be null, not throw + Assert.NotNull(chatMessage.Contents); // Contents should be initialized + } + + [Fact] + public void ChatMessage_WithEmptyContents_TextIsNull() + { + // Arrange - Simulate Mistral SDK response with empty contents + var chatMessage = new ChatMessage + { + Role = ChatRole.Assistant, + Contents = [] + }; + + // Act & Assert + Assert.Null(chatMessage.Text); // Text derived from empty Contents should be null + } + + [Fact] + public void ChatResponse_WithNullTextMessage_LastOrDefaultTextAccess() + { + // Arrange - Simulate what CellmFunctions.GetResponseAsync does at line 317 + // var assistantMessage = response.Messages.LastOrDefault()?.Text ?? throw new InvalidOperationException("No text response"); + var messages = new List + { + new(ChatRole.User, "Hello"), + new(ChatRole.Assistant, (string?)null) // Simulating Mistral SDK returning null text + }; + + var chatResponse = new ChatResponse(messages); + + // Act - This is what CellmFunctions does + var assistantMessage = chatResponse.Messages.LastOrDefault()?.Text; + + // Assert - This should not throw, but should return null + Assert.Null(assistantMessage); + } + + /// + /// This test demonstrates the exact flow that causes issue #309. + /// When Mistral SDK returns a ChatMessage with null/empty content, + /// the following code in CellmFunctions.cs:317 will throw InvalidOperationException: + /// + /// var assistantMessage = response.Messages.LastOrDefault()?.Text + /// ?? throw new InvalidOperationException("No text response"); + /// + /// However, the "Object reference not set to an instance of an object" error + /// suggests that somewhere in the pipeline, null is being dereferenced. + /// This could be in: + /// 1. MistralThinkingBehavior.After() - accessing .Text.Trim() on null Text + /// 2. Other behaviors that process the response + /// + [Fact] + public void Prompt_WithNullTextAssistantMessage_AddedMessages() + { + // Arrange + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("Hello") + .Build(); + + // Simulate adding a response message with null text (from Mistral SDK) + var responseMessage = new ChatMessage(ChatRole.Assistant, (string?)null); + + // Act - Simulate what ProviderRequestHandler does + var newPrompt = new PromptBuilder(prompt) + .AddMessage(responseMessage) + .Build(); + + // Assert + Assert.Equal(2, newPrompt.Messages.Count); + Assert.Null(newPrompt.Messages.Last().Text); + } + + /// + /// Tests the scenario where Mistral SDK returns a ChatResponse with messages + /// but the ChatResponse constructor may have issues with null content. + /// + [Fact] + public void ChatResponse_Construction_WithNullTextMessages() + { + // Arrange + var assistantMessage = new ChatMessage(ChatRole.Assistant, (string?)null); + + // Act - Create ChatResponse like the SDK would + var chatResponse = new ChatResponse(assistantMessage); + + // Assert + Assert.Single(chatResponse.Messages); + Assert.Null(chatResponse.Messages.First().Text); + } + + /// + /// Mock test to verify IChatClient returns proper response. + /// This tests what happens when GetResponseAsync returns a message with null text. + /// + [Fact] + public async Task MockedChatClient_ReturnsNullTextResponse_HandledGracefully() + { + // Arrange + var mockChatClient = Substitute.For(); + var expectedResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, (string?)null)); + + mockChatClient.GetResponseAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(expectedResponse)); + + var messages = new List { new(ChatRole.User, "Hello") }; + var options = new ChatOptions { ModelId = "mistral-small-latest" }; + + // Act + var response = await mockChatClient.GetResponseAsync(messages, options, CancellationToken.None); + + // Assert + Assert.NotNull(response); + Assert.Single(response.Messages); + var assistantMessage = response.Messages.Last(); + Assert.Equal(ChatRole.Assistant, assistantMessage.Role); + Assert.Null(assistantMessage.Text); // Key assertion - text should be null without throwing + } + + /// + /// Test the exact pattern used in CellmFunctions.cs:317 + /// This should throw InvalidOperationException, not NullReferenceException. + /// If NullReferenceException is thrown, it indicates an issue in the SDK or behaviors. + /// + [Fact] + public void CellmFunctionsPattern_WithNullText_ThrowsInvalidOperationException() + { + // Arrange - Simulate the exact scenario from CellmFunctions.cs + var prompt = new PromptBuilder() + .SetModel("mistral-small-latest") + .AddUserMessage("Hello") + .Build(); + + // Simulate Mistral response with null text + prompt.Messages.Add(new ChatMessage(ChatRole.Assistant, (string?)null)); + + // Act & Assert - Using the exact pattern from CellmFunctions.cs:317 + var exception = Assert.Throws(() => + { + var assistantMessage = prompt.Messages.LastOrDefault()?.Text + ?? throw new InvalidOperationException("No text response"); + }); + + Assert.Equal("No text response", exception.Message); + } +} From 250b4a0a067af414f017a96a90d905763a2e7f94 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 09:10:07 +0000 Subject: [PATCH 2/3] test: Add root cause analysis tests for Mistral issue #309 ROOT CAUSE IDENTIFIED: The bug is in Mistral.SDK v2.3.0's CompletionsEndpoint.ChatClient.cs in the ProcessResponseContent method: ```csharp foreach (var content in response.Choices) { if (content.Message.ToolCalls is not null) // BUG: No null check! ``` When the Mistral API returns a response where Choice.Message is null (which can happen in edge cases), accessing content.Message.ToolCalls throws NullReferenceException: "Object reference not set to an instance of an object" COMPARISON: - Streaming code (CORRECT): choice.Delta?.ToolCalls (null-conditional) - Non-streaming (BUG): content.Message.ToolCalls (no null check) The tests simulate the exact JSON response that triggers the bug by creating a response without a message field, which deserializes to null. FIX: The SDK should use content.Message?.ToolCalls instead. --- .../MistralSdkBugReproductionTests.cs | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 src/Cellm.Tests/Unit/Providers/MistralSdkBugReproductionTests.cs diff --git a/src/Cellm.Tests/Unit/Providers/MistralSdkBugReproductionTests.cs b/src/Cellm.Tests/Unit/Providers/MistralSdkBugReproductionTests.cs new file mode 100644 index 0000000..c9e865f --- /dev/null +++ b/src/Cellm.Tests/Unit/Providers/MistralSdkBugReproductionTests.cs @@ -0,0 +1,265 @@ +using Microsoft.Extensions.AI; +using System.Text.Json; +using Xunit; + +namespace Cellm.Tests.Unit.Providers; + +/// +/// Tests to reproduce the exact bug in Mistral.SDK v2.3.0 that causes issue #309. +/// +/// ROOT CAUSE ANALYSIS: +/// ==================== +/// The bug is in Mistral.SDK's CompletionsEndpoint.ChatClient.cs in the ProcessResponseContent method: +/// +/// ```csharp +/// private static List<AIContent> ProcessResponseContent(ChatCompletionResponse response) +/// { +/// List<AIContent> contents = new(); +/// foreach (var content in response.Choices) +/// { +/// if (content.Message.ToolCalls is not null) // <-- BUG: No null check on content.Message! +/// { +/// contents.Add(new TextContent(content.Message.Content)); +/// // ... +/// } +/// else +/// { +/// contents.Add(new TextContent(content.Message.Content)); +/// } +/// } +/// return contents; +/// } +/// ``` +/// +/// When the Mistral API returns a response where Choice.Message is null (which can happen +/// in certain edge cases), accessing content.Message.ToolCalls throws NullReferenceException. +/// +/// The streaming code in GetStreamingResponseAsync correctly uses choice.Delta?.ToolCalls +/// with null-conditional operator, but ProcessResponseContent does NOT. +/// +/// COMPARISON: +/// - Streaming (CORRECT): choice.Delta?.ToolCalls +/// - Non-streaming (BUG): content.Message.ToolCalls +/// +public class MistralSdkBugReproductionTests +{ + /// + /// Simulates the exact bug in ProcessResponseContent when Choice.Message is null. + /// This demonstrates the NullReferenceException that causes issue #309. + /// + [Fact] + public void ProcessResponseContent_WhenMessageIsNull_ThrowsNullReferenceException() + { + // Arrange - Simulate Mistral API JSON response with null message + // This can happen in certain edge cases like incomplete responses or API errors + var jsonResponse = """ + { + "id": "test-id", + "object": "chat.completion", + "created": 1234567890, + "model": "mistral-small-latest", + "choices": [ + { + "index": 0, + "finish_reason": "stop" + } + ] + } + """; + // Note: "message" field is missing, which deserializes to null + + var response = JsonSerializer.Deserialize(jsonResponse); + + // Act & Assert - This simulates what ProcessResponseContent does + var exception = Record.Exception(() => + { + var contents = new List(); + foreach (var choice in response!.Choices!) + { + // This is the exact bug: no null check before accessing Message.ToolCalls + if (choice.Message.ToolCalls is not null) // CRASH HERE! + { + contents.Add(new TextContent(choice.Message.Content)); + } + else + { + contents.Add(new TextContent(choice.Message.Content)); + } + } + }); + + // The bug causes NullReferenceException + Assert.NotNull(exception); + Assert.IsType(exception); + } + + /// + /// Demonstrates the correct fix: using null-conditional operator like the streaming code does. + /// + [Fact] + public void ProcessResponseContent_WithNullCheck_DoesNotThrow() + { + // Arrange - Same response with null message + var jsonResponse = """ + { + "id": "test-id", + "object": "chat.completion", + "created": 1234567890, + "model": "mistral-small-latest", + "choices": [ + { + "index": 0, + "finish_reason": "stop" + } + ] + } + """; + + var response = JsonSerializer.Deserialize(jsonResponse); + + // Act - Using the CORRECT pattern with null-conditional operator + var exception = Record.Exception(() => + { + var contents = new List(); + foreach (var choice in response!.Choices!) + { + // FIXED: Using null-conditional operator like streaming code does + if (choice.Message?.ToolCalls is not null) // Safe! + { + contents.Add(new TextContent(choice.Message?.Content)); + } + else + { + contents.Add(new TextContent(choice.Message?.Content)); + } + } + }); + + // No exception when using proper null checking + Assert.Null(exception); + } + + /// + /// Tests with a response that has a message but null content (another edge case). + /// This is valid according to Mistral API when assistant returns tool_calls instead of content. + /// + [Fact] + public void ProcessResponseContent_WhenContentIsNull_HandledGracefully() + { + // Arrange - Message exists but content is null (common with tool calls) + var jsonResponse = """ + { + "id": "test-id", + "object": "chat.completion", + "created": 1234567890, + "model": "mistral-small-latest", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": null + }, + "finish_reason": "stop" + } + ] + } + """; + + var response = JsonSerializer.Deserialize(jsonResponse); + + // Act - TextContent accepts null + var exception = Record.Exception(() => + { + var contents = new List(); + foreach (var choice in response!.Choices!) + { + if (choice.Message?.ToolCalls is not null) + { + contents.Add(new TextContent(choice.Message?.Content)); + } + else + { + contents.Add(new TextContent(choice.Message?.Content)); + } + } + }); + + // Should not throw - TextContent accepts null + Assert.Null(exception); + } + + /// + /// Tests with an empty choices array - another edge case. + /// + [Fact] + public void ProcessResponseContent_WhenChoicesEmpty_HandledGracefully() + { + var jsonResponse = """ + { + "id": "test-id", + "object": "chat.completion", + "created": 1234567890, + "model": "mistral-small-latest", + "choices": [] + } + """; + + var response = JsonSerializer.Deserialize(jsonResponse); + + var exception = Record.Exception(() => + { + var contents = new List(); + foreach (var choice in response!.Choices!) + { + if (choice.Message?.ToolCalls is not null) + { + contents.Add(new TextContent(choice.Message?.Content)); + } + else + { + contents.Add(new TextContent(choice.Message?.Content)); + } + } + }); + + Assert.Null(exception); + } + + #region Simulated Mistral SDK DTOs + + /// + /// Simulates the Mistral.SDK.DTOs.ChatCompletionResponse class + /// + private class SimulatedChatCompletionResponse + { + public string? Id { get; set; } + public string? Object { get; set; } + public int Created { get; set; } + public string? Model { get; set; } + public List? Choices { get; set; } + } + + /// + /// Simulates the Mistral.SDK.DTOs.Choice class + /// + private class SimulatedChoice + { + public int Index { get; set; } + public SimulatedChatMessage? Message { get; set; } + public string? FinishReason { get; set; } + } + + /// + /// Simulates the Mistral.SDK.DTOs.ChatMessage class + /// + private class SimulatedChatMessage + { + public string? Role { get; set; } + public string? Content { get; set; } + public List? ToolCalls { get; set; } + } + + #endregion +} From 662987cb4eec2bc8191262613e8e8a8a3db4ce8c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 10:28:29 +0000 Subject: [PATCH 3/3] fix: Address CI build and lint errors - Add #pragma warning disable for intentional null dereference in bug reproduction test - Rename async method to use 'Async' suffix per naming conventions --- src/Cellm.Tests/Unit/Providers/MistralProviderTests.cs | 2 +- .../Unit/Providers/MistralSdkBugReproductionTests.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Cellm.Tests/Unit/Providers/MistralProviderTests.cs b/src/Cellm.Tests/Unit/Providers/MistralProviderTests.cs index 1fecb0f..a0f1910 100644 --- a/src/Cellm.Tests/Unit/Providers/MistralProviderTests.cs +++ b/src/Cellm.Tests/Unit/Providers/MistralProviderTests.cs @@ -130,7 +130,7 @@ public void ChatResponse_Construction_WithNullTextMessages() /// This tests what happens when GetResponseAsync returns a message with null text. /// [Fact] - public async Task MockedChatClient_ReturnsNullTextResponse_HandledGracefully() + public async Task MockedChatClient_ReturnsNullTextResponse_HandledGracefullyAsync() { // Arrange var mockChatClient = Substitute.For(); diff --git a/src/Cellm.Tests/Unit/Providers/MistralSdkBugReproductionTests.cs b/src/Cellm.Tests/Unit/Providers/MistralSdkBugReproductionTests.cs index c9e865f..8077fac 100644 --- a/src/Cellm.Tests/Unit/Providers/MistralSdkBugReproductionTests.cs +++ b/src/Cellm.Tests/Unit/Providers/MistralSdkBugReproductionTests.cs @@ -77,6 +77,8 @@ public void ProcessResponseContent_WhenMessageIsNull_ThrowsNullReferenceExceptio foreach (var choice in response!.Choices!) { // This is the exact bug: no null check before accessing Message.ToolCalls + // We deliberately access the null reference to reproduce the bug +#pragma warning disable CS8602 // Dereference of a possibly null reference - intentional to reproduce bug if (choice.Message.ToolCalls is not null) // CRASH HERE! { contents.Add(new TextContent(choice.Message.Content)); @@ -85,6 +87,7 @@ public void ProcessResponseContent_WhenMessageIsNull_ThrowsNullReferenceExceptio { contents.Add(new TextContent(choice.Message.Content)); } +#pragma warning restore CS8602 } });