Skip to content
Merged
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
87 changes: 87 additions & 0 deletions CodexSharpSDK.Extensions.AI.Tests/ChatMessageMapperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using ManagedCode.CodexSharpSDK.Extensions.AI.Internal;
using ManagedCode.CodexSharpSDK.Models;
using Microsoft.Extensions.AI;

namespace ManagedCode.CodexSharpSDK.Extensions.AI.Tests;

public class ChatMessageMapperTests
{
[Test]
public async Task ToCodexInput_TextOnly_ReturnsPrompt()
{
var messages = new[] { new ChatMessage(ChatRole.User, "Hello world") };
var (prompt, images) = ChatMessageMapper.ToCodexInput(messages);
await Assert.That(prompt).IsEqualTo("Hello world");
await Assert.That(images).Count().IsEqualTo(0);
}

[Test]
public async Task ToCodexInput_SystemAndUser_PrependsSystemPrefix()
{
var messages = new[]
{
new ChatMessage(ChatRole.System, "You are helpful"),
new ChatMessage(ChatRole.User, "Help me"),
};
var (prompt, _) = ChatMessageMapper.ToCodexInput(messages);
await Assert.That(prompt).Contains("[System] You are helpful");
await Assert.That(prompt).Contains("Help me");
}

[Test]
public async Task ToCodexInput_AssistantMessage_AppendsAssistantPrefix()
{
var messages = new[]
{
new ChatMessage(ChatRole.User, "Question"),
new ChatMessage(ChatRole.Assistant, "Answer"),
new ChatMessage(ChatRole.User, "Follow up"),
};
var (prompt, _) = ChatMessageMapper.ToCodexInput(messages);
await Assert.That(prompt).Contains("[Assistant] Answer");
await Assert.That(prompt).Contains("Follow up");
}

[Test]
public async Task ToCodexInput_ImageContent_ExtractedSeparately()
{
var imageData = new byte[] { 0x89, 0x50, 0x4E, 0x47 }; // PNG header
var messages = new[]
{
new ChatMessage(ChatRole.User,
[
new TextContent("Describe this"),
new DataContent(imageData, "image/png"),
]),
};
var (prompt, images) = ChatMessageMapper.ToCodexInput(messages);
await Assert.That(prompt).Contains("Describe this");
await Assert.That(images).Count().IsEqualTo(1);
}

[Test]
public async Task ToCodexInput_EmptyMessages_ReturnsEmpty()
{
var (prompt, images) = ChatMessageMapper.ToCodexInput([]);
await Assert.That(prompt).IsEqualTo(string.Empty);
await Assert.That(images).Count().IsEqualTo(0);
}

[Test]
public async Task BuildUserInput_NoImages_ReturnsSingleTextInput()
{
var result = ChatMessageMapper.BuildUserInput("Hello", []);
await Assert.That(result).Count().IsEqualTo(1);
await Assert.That(result[0]).IsTypeOf<TextInput>();
}

[Test]
public async Task BuildUserInput_WithImages_ReturnsTextAndImageInputs()
{
var imageData = new byte[] { 0xFF, 0xD8, 0xFF }; // JPEG header
var images = new List<DataContent> { new(imageData, "image/jpeg") };
var result = ChatMessageMapper.BuildUserInput("Look at this", images);
await Assert.That(result.Count).IsGreaterThanOrEqualTo(2);
await Assert.That(result[0]).IsTypeOf<TextInput>();
}
}
53 changes: 53 additions & 0 deletions CodexSharpSDK.Extensions.AI.Tests/ChatOptionsMapperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using ManagedCode.CodexSharpSDK.Client;
using ManagedCode.CodexSharpSDK.Extensions.AI.Internal;
using Microsoft.Extensions.AI;

namespace ManagedCode.CodexSharpSDK.Extensions.AI.Tests;

public class ChatOptionsMapperTests
{
[Test]
public async Task ToThreadOptions_NullOptions_UsesDefaults()
{
var clientOptions = new CodexChatClientOptions { DefaultModel = "test-model" };
var result = ChatOptionsMapper.ToThreadOptions(null, clientOptions);
await Assert.That(result.Model).IsEqualTo("test-model");
}

[Test]
public async Task ToThreadOptions_ModelId_MapsToModel()
{
var chatOptions = new ChatOptions { ModelId = "gpt-5" };
var clientOptions = new CodexChatClientOptions { DefaultModel = "default" };
var result = ChatOptionsMapper.ToThreadOptions(chatOptions, clientOptions);
await Assert.That(result.Model).IsEqualTo("gpt-5");
}

[Test]
public async Task ToThreadOptions_AdditionalProperties_MapsCodexKeys()
{
var chatOptions = new ChatOptions
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
[ChatOptionsMapper.SandboxModeKey] = SandboxMode.WorkspaceWrite,
[ChatOptionsMapper.FullAutoKey] = true,
[ChatOptionsMapper.ProfileKey] = "strict",
[ChatOptionsMapper.ReasoningEffortKey] = ModelReasoningEffort.High,
},
};
var result = ChatOptionsMapper.ToThreadOptions(chatOptions, new CodexChatClientOptions());
await Assert.That(result.SandboxMode).IsEqualTo(SandboxMode.WorkspaceWrite);
await Assert.That(result.FullAuto).IsTrue();
await Assert.That(result.Profile).IsEqualTo("strict");
await Assert.That(result.ModelReasoningEffort).IsEqualTo(ModelReasoningEffort.High);
}

[Test]
public async Task ToTurnOptions_SetsCancellationToken()
{
using var cts = new CancellationTokenSource();
var result = ChatOptionsMapper.ToTurnOptions(null, cts.Token);
await Assert.That(result.CancellationToken).IsEqualTo(cts.Token);
}
}
83 changes: 83 additions & 0 deletions CodexSharpSDK.Extensions.AI.Tests/ChatResponseMapperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using ManagedCode.CodexSharpSDK.Extensions.AI.Content;
using ManagedCode.CodexSharpSDK.Extensions.AI.Internal;
using ManagedCode.CodexSharpSDK.Models;
using Microsoft.Extensions.AI;

namespace ManagedCode.CodexSharpSDK.Extensions.AI.Tests;

public class ChatResponseMapperTests
{
[Test]
public async Task ToChatResponse_BasicResult_MapsCorrectly()
{
var result = new RunResult([], "Hello from Codex", new Usage(100, 10, 50));
var response = ChatResponseMapper.ToChatResponse(result, "thread-123");

await Assert.That(response.Text).Contains("Hello from Codex");
await Assert.That(response.ConversationId).IsEqualTo("thread-123");
await Assert.That(response.Usage).IsNotNull();
await Assert.That(response.Usage!.InputTokenCount).IsEqualTo(100);
await Assert.That(response.Usage!.OutputTokenCount).IsEqualTo(50);
await Assert.That(response.Usage!.TotalTokenCount).IsEqualTo(150);
}

[Test]
public async Task ToChatResponse_NullUsage_NoUsageSet()
{
var result = new RunResult([], "Response", null);
var response = ChatResponseMapper.ToChatResponse(result, null);
await Assert.That(response.Usage).IsNull();
await Assert.That(response.ConversationId).IsNull();
}

[Test]
public async Task ToChatResponse_WithReasoningItem_MapsToTextReasoningContent()
{
var items = new List<ThreadItem>
{
new ReasoningItem("r1", "thinking about this..."),
};
var result = new RunResult(items, "Final answer", null);
var response = ChatResponseMapper.ToChatResponse(result, null);

var contents = response.Messages[0].Contents;
await Assert.That(contents.OfType<TextReasoningContent>().Count()).IsEqualTo(1);
}

[Test]
public async Task ToChatResponse_WithCommandExecution_MapsToCustomContent()
{
var items = new List<ThreadItem>
{
new CommandExecutionItem("c1", "npm test", "all passed", 0, CommandExecutionStatus.Completed),
};
var result = new RunResult(items, "Done", null);
var response = ChatResponseMapper.ToChatResponse(result, null);

var cmdContent = response.Messages[0].Contents.OfType<CommandExecutionContent>().Single();
await Assert.That(cmdContent.Command).IsEqualTo("npm test");
await Assert.That(cmdContent.ExitCode).IsEqualTo(0);
}

[Test]
public async Task ToChatResponse_WithFileChange_MapsToCustomContent()
{
var items = new List<ThreadItem>
{
new FileChangeItem("f1", [new FileUpdateChange("src/app.cs", PatchChangeKind.Update)], PatchApplyStatus.Completed),
};
var result = new RunResult(items, "Fixed", null);
var response = ChatResponseMapper.ToChatResponse(result, null);

var fileContent = response.Messages[0].Contents.OfType<FileChangeContent>().Single();
await Assert.That(fileContent.Changes).Count().IsEqualTo(1);
}

[Test]
public async Task ToChatResponse_CachedTokens_IncludedInUsage()
{
var result = new RunResult([], "Response", new Usage(100, 50, 25));
var response = ChatResponseMapper.ToChatResponse(result, null);
await Assert.That(response.Usage!.CachedInputTokenCount).IsEqualTo(50);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using ManagedCode.CodexSharpSDK.Extensions.AI.Extensions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

namespace ManagedCode.CodexSharpSDK.Extensions.AI.Tests;

public class CodexServiceCollectionExtensionsTests
{
[Test]
public async Task AddCodexChatClient_RegistersIChatClient()
{
var services = new ServiceCollection();
services.AddCodexChatClient();
var provider = services.BuildServiceProvider();
var client = provider.GetService<IChatClient>();
await Assert.That(client).IsNotNull();
await Assert.That(client).IsTypeOf<CodexChatClient>();
}

[Test]
public async Task AddCodexChatClient_WithConfiguration_RegistersIChatClient()
{
var services = new ServiceCollection();
services.AddCodexChatClient(_ => { });
var provider = services.BuildServiceProvider();
var client = provider.GetService<IChatClient>();
await Assert.That(client).IsNotNull();
}

[Test]
public async Task AddKeyedCodexChatClient_RegistersWithKey()
{
var services = new ServiceCollection();
services.AddKeyedCodexChatClient("codex");
var provider = services.BuildServiceProvider();
var client = provider.GetKeyedService<IChatClient>("codex");
await Assert.That(client).IsNotNull();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>ManagedCode.CodexSharpSDK.Extensions.AI.Tests</RootNamespace>
<AssemblyName>ManagedCode.CodexSharpSDK.Extensions.AI.Tests</AssemblyName>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CA1707;CS1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="TUnit" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CodexSharpSDK.Extensions.AI\CodexSharpSDK.Extensions.AI.csproj" />
<ProjectReference Include="..\CodexSharpSDK\CodexSharpSDK.csproj" />
</ItemGroup>
</Project>
92 changes: 92 additions & 0 deletions CodexSharpSDK.Extensions.AI.Tests/StreamingEventMapperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using ManagedCode.CodexSharpSDK.Extensions.AI.Content;
using ManagedCode.CodexSharpSDK.Extensions.AI.Internal;
using ManagedCode.CodexSharpSDK.Models;
using Microsoft.Extensions.AI;

namespace ManagedCode.CodexSharpSDK.Extensions.AI.Tests;

public class StreamingEventMapperTests
{
[Test]
public async Task ToUpdates_ThreadStarted_YieldsConversationId()
{
var events = ToAsyncEnumerable(new ThreadStartedEvent("thread-1"));
var updates = await CollectUpdates(events);
await Assert.That(updates[0].ConversationId).IsEqualTo("thread-1");
}

[Test]
public async Task ToUpdates_AgentMessage_YieldsTextContent()
{
var events = ToAsyncEnumerable(
new ItemCompletedEvent(new AgentMessageItem("m1", "Hello")));
var updates = await CollectUpdates(events);
await Assert.That(updates[0].Text).IsEqualTo("Hello");
await Assert.That(updates[0].Role).IsEqualTo(ChatRole.Assistant);
}

[Test]
public async Task ToUpdates_TurnCompleted_YieldsFinishReason()
{
var events = ToAsyncEnumerable(
new TurnCompletedEvent(new Usage(10, 0, 5)));
var updates = await CollectUpdates(events);
await Assert.That(updates[0].FinishReason).IsEqualTo(ChatFinishReason.Stop);
}

[Test]
public async Task ToUpdates_TurnFailed_ThrowsException()
{
var events = ToAsyncEnumerable(
new TurnFailedEvent(new ThreadError("something broke")));

await Assert.That(async () => await CollectUpdates(events))
.ThrowsExactly<InvalidOperationException>();
}

[Test]
public async Task ToUpdates_CommandExecution_YieldsCustomContent()
{
var events = ToAsyncEnumerable(
new ItemCompletedEvent(
new CommandExecutionItem("c1", "ls", "file.txt", 0, CommandExecutionStatus.Completed)));
var updates = await CollectUpdates(events);
var content = updates[0].Contents.OfType<CommandExecutionContent>().Single();
await Assert.That(content.Command).IsEqualTo("ls");
}

[Test]
public async Task ToUpdates_FullSequence_MapsAllEvents()
{
var events = ToAsyncEnumerable(
new ThreadStartedEvent("t1"),
new TurnStartedEvent(),
new ItemCompletedEvent(new ReasoningItem("r1", "thinking")),
new ItemCompletedEvent(new AgentMessageItem("m1", "answer")),
new TurnCompletedEvent(new Usage(10, 0, 5)));

var updates = await CollectUpdates(events);
// TurnStartedEvent is not matched in the switch, so 4 updates expected
await Assert.That(updates.Count).IsGreaterThanOrEqualTo(4);
}

private static async IAsyncEnumerable<ThreadEvent> ToAsyncEnumerable(params ThreadEvent[] events)
{
foreach (var evt in events)
{
yield return evt;
await Task.CompletedTask;
}
}

private static async Task<List<ChatResponseUpdate>> CollectUpdates(IAsyncEnumerable<ThreadEvent> events)
{
var updates = new List<ChatResponseUpdate>();
await foreach (var update in StreamingEventMapper.ToUpdates(events))
{
updates.Add(update);
}

return updates;
}
}
Loading
Loading