tests: work on adding integration tests

This commit is contained in:
Stevan Freeborn
2024-07-03 00:11:48 -05:00
parent 78497b8235
commit 917719d36e
7 changed files with 625 additions and 4 deletions
@@ -31,4 +31,231 @@ public class AnthropicApiClientTests : IntegrationTest
var actualErrorType = result.Error.Error!.GetType();
actualErrorType.Should().Be(errorType);
}
[Fact]
public async Task CreateChatMessageAsync_WhenCalledAndMessageCreatedWithTextContent_ItShouldReturnMessage()
{
_mockHttpMessageHandler
.WhenCreateMessageRequest()
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""content"": [
{
""text"": ""Hi! My name is Claude."",
""type"": ""text""
}
],
""id"": ""msg_013Zva2CMHLNnXjNJJKqJ2EF"",
""model"": ""claude-3-5-sonnet-20240620"",
""role"": ""assistant"",
""stop_reason"": ""end_turn"",
""stop_sequence"": null,
""type"": ""message"",
""usage"": {
""input_tokens"": 10,
""output_tokens"": 25
}
}"
);
var request = new ChatMessageRequest(
model: AnthropicModels.Claude35Sonnet,
messages: [new(MessageRole.User, [new TextContent("Hello!")])]
);
var result = await Client.CreateChatMessageAsync(request);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<ChatResponse>();
result.Error.Should().BeNull();
var message = result.Value;
message.Id.Should().Be("msg_013Zva2CMHLNnXjNJJKqJ2EF");
message.Model.Should().Be("claude-3-5-sonnet-20240620");
message.Role.Should().Be("assistant");
message.StopReason.Should().Be("end_turn");
message.StopSequence.Should().BeNull();
message.Type.Should().Be("message");
message.Usage.InputTokens.Should().Be(10);
message.Usage.OutputTokens.Should().Be(25);
message.Content.Should().HaveCount(1);
message.ToolCall.Should().BeNull();
var textContent = message.Content[0];
textContent.Should().BeOfType<TextContent>();
textContent.As<TextContent>().Text.Should().Be("Hi! My name is Claude.");
textContent.As<TextContent>().Type.Should().Be("text");
}
[Fact]
public async Task CreateChatMessageAsync_WhenCalledAndMessageCreatedWithToolUseContentAndToolIsProvided_ItShouldReturnMessageWithToolCall()
{
_mockHttpMessageHandler
.WhenCreateMessageRequest()
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""content"": [
{
""id"": ""toolu_01D7FLrfh4GYq7yT1ULFeyMV"",
""name"": ""get_stock_price"",
""input"": { ""ticker"": ""^GSPC"" },
""type"": ""tool_use""
}
],
""id"": ""msg_01D7FLrfh4GYq7yT1ULFeyMV"",
""model"": ""claude-3-5-sonnet-20240620"",
""role"": ""assistant"",
""stop_reason"": ""end_turn"",
""stop_sequence"": null,
""type"": ""message"",
""usage"": {
""input_tokens"": 10,
""output_tokens"": 25
}
}"
);
var func = (string ticker) => ticker;
var request = new ChatMessageRequest(
model: AnthropicModels.Claude35Sonnet,
messages: [
new(MessageRole.User, [new TextContent("Hello!")]),
],
tools: [
Tool.CreateFromFunction("get_stock_price", "Gets the stock price for a given ticker", func)
]
);
var result = await Client.CreateChatMessageAsync(request);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<ChatResponse>();
result.Error.Should().BeNull();
var message = result.Value;
message.Id.Should().Be("msg_01D7FLrfh4GYq7yT1ULFeyMV");
message.Model.Should().Be("claude-3-5-sonnet-20240620");
message.Role.Should().Be("assistant");
message.StopReason.Should().Be("end_turn");
message.StopSequence.Should().BeNull();
message.Type.Should().Be("message");
message.Usage.InputTokens.Should().Be(10);
message.Usage.OutputTokens.Should().Be(25);
message.Content.Should().HaveCount(1);
message.ToolCall.Should().NotBeNull();
var toolUseContent = message.Content[0];
toolUseContent.Should().BeOfType<ToolUseContent>();
toolUseContent.As<ToolUseContent>().Id.Should().Be("toolu_01D7FLrfh4GYq7yT1ULFeyMV");
toolUseContent.As<ToolUseContent>().Name.Should().Be("get_stock_price");
toolUseContent.As<ToolUseContent>().Type.Should().Be("tool_use");
var input = toolUseContent.As<ToolUseContent>().Input;
var ticker = input.GetValueOrDefault("ticker");
ticker!.ToString().Should().Be("^GSPC");
var toolCallResult = await message.ToolCall!.InvokeAsync();
toolCallResult.IsSuccess.Should().BeTrue();
toolCallResult.Value!.ToString().Should().Be("^GSPC");
toolCallResult.Error.Should().BeNull();
}
[Fact]
public async Task CreateChatMessageAsync_WhenCalledAndMessageCreatedWithToolUseButNoToolProvided_ItShouldReturnMessageWithoutToolCall()
{
_mockHttpMessageHandler
.WhenCreateMessageRequest()
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""content"": [
{
""id"": ""toolu_01D7FLrfh4GYq7yT1ULFeyMV"",
""name"": ""get_stock_price"",
""input"": { ""ticker"": ""^GSPC"" },
""type"": ""tool_use""
}
],
""id"": ""msg_01D7FLrfh4GYq7yT1ULFeyMV"",
""model"": ""claude-3-5-sonnet-20240620"",
""role"": ""assistant"",
""stop_reason"": ""end_turn"",
""stop_sequence"": null,
""type"": ""message"",
""usage"": {
""input_tokens"": 10,
""output_tokens"": 25
}
}"
);
var request = new ChatMessageRequest(
model: AnthropicModels.Claude35Sonnet,
messages: [
new(MessageRole.User, [new TextContent("Hello!")]),
]
);
var result = await Client.CreateChatMessageAsync(request);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<ChatResponse>();
result.Error.Should().BeNull();
var message = result.Value;
message.Id.Should().Be("msg_01D7FLrfh4GYq7yT1ULFeyMV");
message.Model.Should().Be("claude-3-5-sonnet-20240620");
message.Role.Should().Be("assistant");
message.StopReason.Should().Be("end_turn");
message.StopSequence.Should().BeNull();
message.Type.Should().Be("message");
message.Usage.InputTokens.Should().Be(10);
message.Usage.OutputTokens.Should().Be(25);
message.Content.Should().HaveCount(1);
message.ToolCall.Should().BeNull();
var toolUseContent = message.Content[0];
toolUseContent.Should().BeOfType<ToolUseContent>();
toolUseContent.As<ToolUseContent>().Id.Should().Be("toolu_01D7FLrfh4GYq7yT1ULFeyMV");
toolUseContent.As<ToolUseContent>().Name.Should().Be("get_stock_price");
toolUseContent.As<ToolUseContent>().Type.Should().Be("tool_use");
var input = toolUseContent.As<ToolUseContent>().Input;
var ticker = input.GetValueOrDefault("ticker");
ticker!.ToString().Should().Be("^GSPC");
}
[Theory]
[ClassData(typeof(EventTestData))]
public async Task CreateChatMessageAsync_WhenCalledAndMessageIsStreamed_ItShouldReturnAllEvents(string eventString, AnthropicEvent anthropicEvent)
{
_mockHttpMessageHandler
.WhenCreateStreamMessageRequest()
.Respond(
HttpStatusCode.OK,
"text/event-stream",
new MemoryStream(Encoding.UTF8.GetBytes(eventString))
);
var request = new StreamChatMessageRequest(
model: AnthropicModels.Claude35Sonnet,
messages: [
new(MessageRole.User, [new TextContent("Hello!")]),
]
);
var result = Client.CreateChatMessageAsync(request);
var e = await result.FirstOrDefaultAsync();
e.Should().BeEquivalentTo(anthropicEvent);
}
}
@@ -0,0 +1,385 @@
namespace AnthropicClient.Tests.Integration;
public class EventTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[]
{
"""
event: message_start
data: {"type":"message_start","message":{"id":"msg_014p7gG3wDgGV9EUtLvnow3U","type":"message","role":"assistant","model":"claude-3-haiku-20240307","stop_sequence":null,"usage":{"input_tokens":472,"output_tokens":2},"content":[],"stop_reason":null}}
""",
new AnthropicEvent()
{
Type = EventType.MessageStart,
Data = new MessageStartEventData()
{
Message = new ChatResponse()
{
Id = "msg_014p7gG3wDgGV9EUtLvnow3U",
Type = "message",
Role = "assistant",
Model = "claude-3-haiku-20240307",
StopSequence = null,
Usage = new ChatUsage()
{
InputTokens = 472,
OutputTokens = 2,
},
Content = [],
StopReason = null,
},
},
},
};
yield return new object[]
{
"""
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockStart,
Data = new ContentStartEventData()
{
Index = 0,
ContentBlock = new TextContent()
{
Type = "text",
Text = "",
},
},
},
};
yield return new object[]
{
"""
event: ping
data: {"type": "ping"}
""",
new AnthropicEvent()
{
Type = EventType.Ping,
Data = new PingEventData(),
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Okay"}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = "Okay",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":","}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = ",",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" let"}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = " let",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'s"}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = "'s",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" check"}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = " check",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the"}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = " the",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" weather"}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = " weather",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for"}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = " for",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" San"}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = " San",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Francisco"}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = " Francisco",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":","}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = " ,",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" CA"}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = " CA",
},
},
},
};
yield return new object[]
{
"""
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":":"}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockDelta,
Data = new ContentDeltaEventData()
{
Index = 0,
Delta = new TextDelta()
{
Type = "text_delta",
Text = " :",
},
},
},
};
yield return new object[]
{
"""
event: content_block_stop
data: {"type":"content_block_stop","index":0}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockStop,
Data = new ContentStopEventData()
{
Index = 0,
},
},
};
yield return new object[]
{
"""
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01T1x1fJ34qAmk2tNTrN7Up6","name":"get_weather","input":{}}}
""",
new AnthropicEvent()
{
Type = EventType.ContentBlockStart,
Data = new ContentStartEventData()
{
Index = 1,
ContentBlock = new ToolUseContent()
{
Type = "tool_use",
Id = "toolu_01T1x1fJ34qAmk2tNTrN7Up6",
Name = "get_weather",
Input = [],
},
},
},
};
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
@@ -28,13 +28,13 @@ public static class MockHttpMessageHandlerExtensions
{
return mockHttpMessageHandler
.SetupBaseRequest()
.WithJsonContent<ChatMessageRequest>(r => r.Stream == false);
.WithJsonContent<ChatMessageRequest>(r => r.Stream == false, JsonSerializationOptions.DefaultOptions);
}
public static MockedRequest WhenCreateStreamMessageRequest(this MockHttpMessageHandler mockHttpMessageHandler)
{
return mockHttpMessageHandler
.SetupBaseRequest()
.WithJsonContent<ChatMessageRequest>(r => r.Stream == true);
.WithJsonContent<StreamChatMessageRequest>(r => r.Stream == true, JsonSerializationOptions.DefaultOptions);
}
}