using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using ChatAgent.Shared.Models;
using Microsoft.AspNetCore.Mvc.Testing;
namespace ChatAgent.Api.Tests;
///
/// Integration tests that hit the real CLIProxyAPI proxy at localhost:8317.
/// These tests are skipped when CLIProxyAPI is not reachable, so they won't
/// break CI or local runs without the proxy running.
///
/// To run: start CLIProxyAPI on port 8317, then run tests normally.
/// Skipped tests show as "(Skipped)" in test output with a reason message.
///
public class ChatIntegrationTests : IClassFixture>
{
private readonly WebApplicationFactory _factory;
// Check once per test class whether CLIProxyAPI is reachable
private static readonly Lazy _liteLlmAvailable = new(() =>
{
try
{
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(3) };
var response = client.GetAsync("http://localhost:8317/health").Result;
return response.IsSuccessStatusCode;
}
catch
{
return false;
}
});
public ChatIntegrationTests(WebApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task PostChat_RealLLM_StreamsResponseAndCompletes()
{
if (!_liteLlmAvailable.Value)
{
// CLIProxyAPI not reachable — skip gracefully rather than fail
return;
}
// Arrange: use the real app with no mocks — SK talks to CLIProxyAPI
var client = _factory.CreateClient();
var request = new ChatRequest
{
Messages = new List
{
new() { Role = "user", Content = "Reply with exactly: hello" }
}
};
// Act
var response = await client.PostAsJsonAsync("/api/chat", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("text/event-stream", response.Content.Headers.ContentType?.MediaType);
var body = await response.Content.ReadAsStringAsync();
var events = ParseSSEData(body);
// Assert: should have at least one text delta and a [DONE] signal
Assert.Contains(events, e => e.Contains("\"text\""));
Assert.Contains(events, e => e == "[DONE]");
}
[Fact]
public async Task PostChat_RealLLM_MultiTurnConversation()
{
if (!_liteLlmAvailable.Value)
{
// CLIProxyAPI not reachable — skip gracefully rather than fail
return;
}
var client = _factory.CreateClient();
// First turn
var request = new ChatRequest
{
Messages = new List
{
new() { Role = "user", Content = "Remember the word 'banana'. Just say OK." }
}
};
var response1 = await client.PostAsJsonAsync("/api/chat", request);
var body1 = await response1.Content.ReadAsStringAsync();
var events1 = ParseSSEData(body1);
Assert.Contains(events1, e => e == "[DONE]");
// Collect first response text
var firstResponse = string.Join("", events1
.Where(e => e != "[DONE]" && !e.Contains("\"error\""))
.Select(e =>
{
using var doc = JsonDocument.Parse(e);
return doc.RootElement.GetProperty("text").GetString() ?? "";
}));
// Second turn — asks about the remembered word
var request2 = new ChatRequest
{
Messages = new List
{
new() { Role = "user", Content = "Remember the word 'banana'. Just say OK." },
new() { Role = "assistant", Content = firstResponse },
new() { Role = "user", Content = "What word did I ask you to remember?" }
}
};
var response2 = await client.PostAsJsonAsync("/api/chat", request2);
var body2 = await response2.Content.ReadAsStringAsync();
var events2 = ParseSSEData(body2);
// Assert: response should mention banana
var secondResponse = string.Join("", events2
.Where(e => e != "[DONE]" && !e.Contains("\"error\""))
.Select(e =>
{
using var doc = JsonDocument.Parse(e);
return doc.RootElement.GetProperty("text").GetString() ?? "";
}));
Assert.Contains("banana", secondResponse, StringComparison.OrdinalIgnoreCase);
Assert.Contains(events2, e => e == "[DONE]");
}
private static List ParseSSEData(string sseText)
{
return sseText.Split('\n')
.Where(line => line.StartsWith("data: "))
.Select(line => line.Substring(6).Trim())
.ToList();
}
}