Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,83 @@ public async Task IfAutoInvokeShouldAllowFilterToModifyFunctionResultAsync()
Assert.Contains(ModifiedResult, secondRequestContent);
}

[Fact]
public async Task FunctionCallWithThoughtSignatureIsCapturedInToolCallAsync()
{
// Arrange
var responseWithThoughtSignature = File.ReadAllText("./TestData/chat_function_with_thought_signature_response.json")
.Replace("%nameSeparator%", GeminiFunction.NameSeparator, StringComparison.Ordinal);
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(responseWithThoughtSignature);

var client = this.CreateChatCompletionClient();
var chatHistory = CreateSampleChatHistory();
var executionSettings = new GeminiPromptExecutionSettings
{
ToolCallBehavior = GeminiToolCallBehavior.EnableFunctions([this._timePluginNow])
};

// Act
var messages = await client.GenerateChatMessageAsync(chatHistory, executionSettings: executionSettings, kernel: this._kernelWithFunctions);

// Assert
Assert.Single(messages);
var geminiMessage = messages[0] as GeminiChatMessageContent;
Assert.NotNull(geminiMessage);
Assert.NotNull(geminiMessage.ToolCalls);
Assert.Single(geminiMessage.ToolCalls);
Assert.Equal("test-thought-signature-abc123", geminiMessage.ToolCalls[0].ThoughtSignature);
}

[Fact]
public async Task TextResponseWithThoughtSignatureIsCapturedInMetadataAsync()
{
// Arrange
var responseWithThoughtSignature = File.ReadAllText("./TestData/chat_text_with_thought_signature_response.json");
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(responseWithThoughtSignature);

var client = this.CreateChatCompletionClient();
var chatHistory = CreateSampleChatHistory();

// Act
var messages = await client.GenerateChatMessageAsync(chatHistory);

// Assert
Assert.Single(messages);
var geminiMessage = messages[0] as GeminiChatMessageContent;
Assert.NotNull(geminiMessage);
Assert.NotNull(geminiMessage.Metadata);
var metadata = geminiMessage.Metadata as GeminiMetadata;
Assert.NotNull(metadata);
Assert.Equal("text-response-thought-signature-xyz789", metadata.ThoughtSignature);
}

[Fact]
public async Task ThoughtSignatureIsIncludedInSubsequentRequestAsync()
{
// Arrange - First response has function call with ThoughtSignature
var responseWithThoughtSignature = File.ReadAllText("./TestData/chat_function_with_thought_signature_response.json")
.Replace("%nameSeparator%", GeminiFunction.NameSeparator, StringComparison.Ordinal);
using var handlerStub = new MultipleHttpMessageHandlerStub();
handlerStub.AddJsonResponse(responseWithThoughtSignature);
handlerStub.AddJsonResponse(this._responseContent); // Second response is text

using var httpClient = new HttpClient(handlerStub, false);
var client = this.CreateChatCompletionClient(httpClient: httpClient);
var chatHistory = CreateSampleChatHistory();
var executionSettings = new GeminiPromptExecutionSettings
{
ToolCallBehavior = GeminiToolCallBehavior.AutoInvokeKernelFunctions
};

// Act
await client.GenerateChatMessageAsync(chatHistory, executionSettings: executionSettings, kernel: this._kernelWithFunctions);

// Assert - Check that the second request includes the ThoughtSignature
var secondRequestContent = handlerStub.GetRequestContentAsString(1);
Assert.NotNull(secondRequestContent);
Assert.Contains("test-thought-signature-abc123", secondRequestContent);
}

private static ChatHistory CreateSampleChatHistory()
{
var chatHistory = new ChatHistory();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,63 @@ await client.StreamGenerateChatMessageAsync(chatHistory, executionSettings: exec
c is GeminiChatMessageContent gm && gm.Role == AuthorRole.Tool && gm.CalledToolResult is not null);
}

[Fact]
public async Task StreamingTextResponseWithAutoInvokeAndEmptyToolCallsDoesNotEnterToolCallingBranchAsync()
{
// Arrange - This tests the Phase 6 bug fix: empty ToolCalls list should not trigger tool calling
var client = this.CreateChatCompletionClient();
var chatHistory = CreateSampleChatHistory();
var executionSettings = new GeminiPromptExecutionSettings
{
ToolCallBehavior = GeminiToolCallBehavior.AutoInvokeKernelFunctions
};

// Response is text-only (no function calls), so ToolCalls will be empty list
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(this._responseContent);

// Act
var messages = await client.StreamGenerateChatMessageAsync(
chatHistory,
executionSettings: executionSettings,
kernel: this._kernelWithFunctions).ToListAsync();

// Assert - Should yield text response without entering tool-calling branch
Assert.NotEmpty(messages);
Assert.All(messages, m =>
{
var geminiMessage = m as GeminiStreamingChatMessageContent;
Assert.NotNull(geminiMessage);
// ToolCalls should be null or empty for text responses
Assert.True(geminiMessage.ToolCalls is null || geminiMessage.ToolCalls.Count == 0);
});
}

[Fact]
public async Task StreamingTextResponseWithAutoInvokeAndNullToolCallsDoesNotEnterToolCallingBranchAsync()
{
// Arrange - This tests that pattern `is { Count: > 0 }` handles null safely
var client = this.CreateChatCompletionClient();
var chatHistory = CreateSampleChatHistory();
var executionSettings = new GeminiPromptExecutionSettings
{
ToolCallBehavior = GeminiToolCallBehavior.AutoInvokeKernelFunctions
};

// Response is text-only
this._messageHandlerStub.ResponseToReturn.Content = new StringContent(this._responseContent);

// Act
var messages = await client.StreamGenerateChatMessageAsync(
chatHistory,
executionSettings: executionSettings,
kernel: this._kernelWithFunctions).ToListAsync();

// Assert - Should complete without errors
Assert.NotEmpty(messages);
// Verify we got text content
Assert.Contains(messages, m => !string.IsNullOrEmpty(m.Content));
}

private static ChatHistory CreateSampleChatHistory()
{
var chatHistory = new ChatHistory();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,71 @@ public void ToStringReturnsCorrectValue()
// Act & Assert
Assert.Equal("MyPlugin_MyFunction(location:San Diego, max_price:300)", functionToolCall.ToString());
}

[Fact]
public void ThoughtSignatureIsNullWhenCreatedFromFunctionCallPart()
{
// Arrange - Using the FunctionCallPart constructor (no ThoughtSignature)
var toolCallPart = new GeminiPart.FunctionCallPart { FunctionName = "MyFunction" };
var functionToolCall = new GeminiFunctionToolCall(toolCallPart);

// Act & Assert
Assert.Null(functionToolCall.ThoughtSignature);
}

[Fact]
public void ThoughtSignatureIsCapturedWhenCreatedFromGeminiPart()
{
// Arrange - Using the GeminiPart constructor (with ThoughtSignature)
var part = new GeminiPart
{
FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "MyFunction" },
ThoughtSignature = "test-thought-signature-123"
};
var functionToolCall = new GeminiFunctionToolCall(part);

// Act & Assert
Assert.Equal("test-thought-signature-123", functionToolCall.ThoughtSignature);
}

[Fact]
public void ThoughtSignatureIsNullWhenGeminiPartHasNoSignature()
{
// Arrange
var part = new GeminiPart
{
FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "MyFunction" },
ThoughtSignature = null
};
var functionToolCall = new GeminiFunctionToolCall(part);

// Act & Assert
Assert.Null(functionToolCall.ThoughtSignature);
}

[Fact]
public void ArgumentsArePreservedWhenCreatedFromGeminiPart()
{
// Arrange
var part = new GeminiPart
{
FunctionCall = new GeminiPart.FunctionCallPart
{
FunctionName = "MyPlugin_MyFunction",
Arguments = new JsonObject
{
{ "location", "San Diego" },
{ "max_price", 300 }
}
},
ThoughtSignature = "signature-abc"
};
var functionToolCall = new GeminiFunctionToolCall(part);

// Act & Assert
Assert.NotNull(functionToolCall.Arguments);
Assert.Equal(2, functionToolCall.Arguments.Count);
Assert.Equal("San Diego", functionToolCall.Arguments["location"]!.ToString());
Assert.Equal("signature-abc", functionToolCall.ThoughtSignature);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.SemanticKernel.Connectors.Google;
using Xunit;

namespace SemanticKernel.Connectors.Google.UnitTests.Core.Gemini;

/// <summary>
/// Unit tests for <see cref="GeminiMetadata"/> class.
/// </summary>
public sealed class GeminiMetadataTests
{
[Fact]
public void ThoughtSignatureCanBeSetAndRetrieved()
{
// Arrange & Act
var metadata = new GeminiMetadata { ThoughtSignature = "test-signature-123" };

// Assert
Assert.Equal("test-signature-123", metadata.ThoughtSignature);
}

[Fact]
public void ThoughtSignatureIsNullByDefault()
{
// Arrange & Act
var metadata = new GeminiMetadata();

// Assert
Assert.Null(metadata.ThoughtSignature);
}

[Fact]
public void ThoughtSignatureIsStoredInDictionary()
{
// Arrange
var metadata = new GeminiMetadata { ThoughtSignature = "dict-signature" };

// Act
var hasKey = metadata.TryGetValue("ThoughtSignature", out var value);

// Assert
Assert.True(hasKey);
Assert.Equal("dict-signature", value);
}

[Fact]
public void ThoughtSignatureCanBeRetrievedFromDictionary()
{
// Arrange - This simulates deserialized metadata
var metadata = new GeminiMetadata { ThoughtSignature = "from-dict" };

// Act
var signature = metadata.ThoughtSignature;

// Assert
Assert.Equal("from-dict", signature);
}
}
Loading