using System.Net;
using System.Text;
using TeamUp.Modules.Integrations.Ai;
using TeamUp.SharedKernel.Ai;
using Xunit;
namespace TeamUp.IntegrationTests;
///
/// The model-client tool-use plumbing: the OpenAI-compatible adapter serializes tools + a tool-use
/// conversation and parses tool calls out of the reply; the deterministic "tooluse" stub drives the
/// loop (ask for a tool, then answer once a result is present).
///
public sealed class ModelClientToolTests
{
[Fact]
public async Task OpenAi_adapter_sends_tools_and_parses_tool_calls()
{
const string reply =
"""{"choices":[{"message":{"role":"assistant","content":null,"tool_calls":[{"id":"call_abc","type":"function","function":{"name":"search_issues","arguments":"{\"q\":\"bug\"}"}}]}}]}""";
var handler = new CapturingHandler(reply);
var client = new OpenAiCompatibleModelClient(new HttpClient(handler));
var request = new ModelRequest(
"openai", "gpt-4o", "sk-test", null, "find bugs", MaxTokens: 512,
Tools: [new ModelTool("search_issues", "Search the tracker.", """{"type":"object"}""")],
Messages: [new ModelMessage("user", "find bugs")]);
var completion = await client.CompleteAsync(request);
Assert.True(completion.Success);
var call = Assert.Single(completion.ToolCalls!);
Assert.Equal("call_abc", call.Id);
Assert.Equal("search_issues", call.Name);
Assert.Contains("bug", call.ArgumentsJson);
// The outgoing body carries the tool definition and the conversation.
Assert.Contains("\"tools\"", handler.LastBody);
Assert.Contains("search_issues", handler.LastBody);
Assert.Contains("find bugs", handler.LastBody);
}
[Fact]
public async Task OpenAi_adapter_returns_plain_text_when_no_tool_calls()
{
const string reply = """{"choices":[{"message":{"role":"assistant","content":"Here are the bugs."}}]}""";
var client = new OpenAiCompatibleModelClient(new HttpClient(new CapturingHandler(reply)));
var completion = await client.CompleteAsync(new ModelRequest("openai", "gpt-4o", "sk", null, "hi"));
Assert.True(completion.Success);
Assert.Equal("Here are the bugs.", completion.Text);
Assert.Null(completion.ToolCalls);
}
[Fact]
public async Task ToolUse_stub_asks_for_a_tool_then_answers_once_a_result_is_present()
{
var stub = new ToolUseStubModelClient();
List tools = [new ModelTool("lookup", null, "{}")];
var first = await stub.CompleteAsync(new ModelRequest(
"tooluse", "m", "", null, "do it", Tools: tools, Messages: [new ModelMessage("user", "do it")]));
var toolCall = Assert.Single(first.ToolCalls!);
Assert.Equal("lookup", toolCall.Name);
Assert.Null(first.Text);
var second = await stub.CompleteAsync(new ModelRequest(
"tooluse", "m", "", null, "do it", Tools: tools,
Messages:
[
new ModelMessage("user", "do it"),
new ModelMessage("assistant", null, first.ToolCalls),
new ModelMessage("tool", "the result", ToolCallId: toolCall.Id),
]));
Assert.Null(second.ToolCalls);
Assert.Contains("do it", second.Text!);
}
private sealed class CapturingHandler(string responseJson) : HttpMessageHandler
{
public string LastBody { get; private set; } = string.Empty;
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
LastBody = await request.Content!.ReadAsStringAsync(cancellationToken);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(responseJson, Encoding.UTF8, "application/json"),
};
}
}
}