Overhaul extraction pipeline with new TradeItem model, conversation flow, and dedicated extraction endpoint. Add sidebar navigation with NavMenu component and landing page. Introduce few-shot prompting service and tests. Add prompt settings and email upload specs. Update OpenSpec tooling with improved export-spec and extract-feature commands. Archive completed changes and export full specs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
319 lines
11 KiB
C#
319 lines
11 KiB
C#
// ExtractionPluginTests.cs -- Tests for the extraction plugin's schema validation
|
|
// and external API tool methods.
|
|
//
|
|
// ValidateSchema is tested directly (no external APIs needed).
|
|
// LookupCounterparty, ValidateTrade, and ValidateCurrency are tested with
|
|
// mocked HttpClients using MockHttpMessageHandler to simulate API responses.
|
|
|
|
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using ChatAgent.Api.Plugins;
|
|
using ChatAgent.Api.Services;
|
|
using ChatAgent.Shared.Models;
|
|
using Moq;
|
|
using Moq.Protected;
|
|
|
|
namespace ChatAgent.Api.Tests;
|
|
|
|
public class ExtractionPluginTests
|
|
{
|
|
// Helper to create an ExtractionPlugin with mocked HttpClients
|
|
private static ExtractionPlugin CreatePluginWithMocks(
|
|
HttpMessageHandler? counterpartyHandler = null,
|
|
HttpMessageHandler? tradeHandler = null,
|
|
HttpMessageHandler? currencyHandler = null)
|
|
{
|
|
var counterpartyClient = new CounterpartyApiClient(
|
|
new HttpClient(counterpartyHandler ?? CreateOkHandler("[]"))
|
|
{ BaseAddress = new Uri("http://test/") });
|
|
|
|
var tradeClient = new TradeApiClient(
|
|
new HttpClient(tradeHandler ?? CreateOkHandler("{\"isValid\":true}"))
|
|
{ BaseAddress = new Uri("http://test/") });
|
|
|
|
var currencyClient = new CurrencyApiClient(
|
|
new HttpClient(currencyHandler ?? CreateOkHandler("{\"isValid\":true}"))
|
|
{ BaseAddress = new Uri("http://test/") });
|
|
|
|
return new ExtractionPlugin(counterpartyClient, tradeClient, currencyClient);
|
|
}
|
|
|
|
// Creates an HttpMessageHandler that returns a 200 OK with the given JSON body
|
|
private static HttpMessageHandler CreateOkHandler(string jsonBody)
|
|
{
|
|
var mock = new Mock<HttpMessageHandler>();
|
|
mock.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent(jsonBody, System.Text.Encoding.UTF8, "application/json")
|
|
});
|
|
return mock.Object;
|
|
}
|
|
|
|
// Creates an HttpMessageHandler that throws HttpRequestException
|
|
private static HttpMessageHandler CreateErrorHandler()
|
|
{
|
|
var mock = new Mock<HttpMessageHandler>();
|
|
mock.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.ThrowsAsync(new HttpRequestException("Connection refused"));
|
|
return mock.Object;
|
|
}
|
|
|
|
// --- ValidateSchema tests ---
|
|
|
|
[Fact]
|
|
public void ValidateSchema_ValidResult_ReturnsValid()
|
|
{
|
|
var plugin = CreatePluginWithMocks();
|
|
var extraction = new ExtractionResult
|
|
{
|
|
Items = new List<TradeItem>
|
|
{
|
|
new()
|
|
{
|
|
Valuedate = "27/11/2025",
|
|
Counterparty = "Assured Guaranty UK Limited",
|
|
TradeId = 79353083,
|
|
DisplayCcy = "GBP",
|
|
Pv = 4562456.0,
|
|
Breakclause = "N"
|
|
}
|
|
}
|
|
};
|
|
|
|
var resultJson = plugin.ValidateSchema(JsonSerializer.Serialize(extraction));
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.True(result.IsValid);
|
|
Assert.Empty(result.Errors);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateSchema_MissingFields_ReturnsErrors()
|
|
{
|
|
var plugin = CreatePluginWithMocks();
|
|
// Missing counterparty and display_ccy
|
|
var extraction = new ExtractionResult
|
|
{
|
|
Items = new List<TradeItem>
|
|
{
|
|
new()
|
|
{
|
|
Valuedate = "27/11/2025",
|
|
TradeId = 79353083,
|
|
Pv = 4562456.0,
|
|
Breakclause = "N"
|
|
}
|
|
}
|
|
};
|
|
|
|
var resultJson = plugin.ValidateSchema(JsonSerializer.Serialize(extraction));
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("counterparty"));
|
|
Assert.Contains(result.Errors, e => e.Contains("display_ccy"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateSchema_InvalidBreakclause_ReturnsError()
|
|
{
|
|
var plugin = CreatePluginWithMocks();
|
|
var extraction = new ExtractionResult
|
|
{
|
|
Items = new List<TradeItem>
|
|
{
|
|
new()
|
|
{
|
|
Valuedate = "27/11/2025",
|
|
Counterparty = "Test Corp",
|
|
TradeId = 12345,
|
|
DisplayCcy = "GBP",
|
|
Pv = 100.0,
|
|
Breakclause = "Maybe"
|
|
}
|
|
}
|
|
};
|
|
|
|
var resultJson = plugin.ValidateSchema(JsonSerializer.Serialize(extraction));
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("'Y' or 'N'"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateSchema_InvalidJson_ReturnsError()
|
|
{
|
|
var plugin = CreatePluginWithMocks();
|
|
var resultJson = plugin.ValidateSchema("not valid json");
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("Invalid JSON"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateSchema_EmptyItems_ReturnsError()
|
|
{
|
|
var plugin = CreatePluginWithMocks();
|
|
var resultJson = plugin.ValidateSchema("{\"items\":[]}");
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("no items"));
|
|
}
|
|
|
|
// --- LookupCounterparty tests ---
|
|
|
|
[Fact]
|
|
public async Task LookupCounterparty_SingleMatch_ReturnsValid()
|
|
{
|
|
var candidates = new List<CandidateMatch>
|
|
{
|
|
new() { Name = "Assured Guaranty UK Ltd", LegalEntity = "AG-UK-001" }
|
|
};
|
|
var handler = CreateOkHandler(JsonSerializer.Serialize(candidates));
|
|
var plugin = CreatePluginWithMocks(counterpartyHandler: handler);
|
|
|
|
var resultJson = await plugin.LookupCounterparty("Assured Guaranty");
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.True(result.IsValid);
|
|
Assert.NotNull(result.Candidates);
|
|
Assert.Single(result.Candidates);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LookupCounterparty_MultipleMatches_ReturnsInvalidWithCandidates()
|
|
{
|
|
var candidates = new List<CandidateMatch>
|
|
{
|
|
new() { Name = "Assured Guaranty UK Ltd", LegalEntity = "AG-UK-001" },
|
|
new() { Name = "Assured Guaranty EU Ltd", LegalEntity = "AG-EU-002" }
|
|
};
|
|
var handler = CreateOkHandler(JsonSerializer.Serialize(candidates));
|
|
var plugin = CreatePluginWithMocks(counterpartyHandler: handler);
|
|
|
|
var resultJson = await plugin.LookupCounterparty("Assured Guaranty");
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.NotNull(result.Candidates);
|
|
Assert.Equal(2, result.Candidates.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LookupCounterparty_ApiError_ReturnsErrorMessage()
|
|
{
|
|
var plugin = CreatePluginWithMocks(counterpartyHandler: CreateErrorHandler());
|
|
|
|
var resultJson = await plugin.LookupCounterparty("Test");
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("Counterparty API unavailable"));
|
|
}
|
|
|
|
// --- ValidateTrade tests ---
|
|
|
|
[Fact]
|
|
public async Task ValidateTrade_ValidId_ReturnsValid()
|
|
{
|
|
var handler = CreateOkHandler("{\"isValid\":true}");
|
|
var plugin = CreatePluginWithMocks(tradeHandler: handler);
|
|
|
|
var resultJson = await plugin.ValidateTrade(79353083);
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.True(result.IsValid);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateTrade_InvalidId_ReturnsError()
|
|
{
|
|
var handler = CreateOkHandler("{\"isValid\":false,\"message\":\"Trade not found\"}");
|
|
var plugin = CreatePluginWithMocks(tradeHandler: handler);
|
|
|
|
var resultJson = await plugin.ValidateTrade(99999);
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("Trade not found"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateTrade_ApiError_ReturnsErrorMessage()
|
|
{
|
|
var plugin = CreatePluginWithMocks(tradeHandler: CreateErrorHandler());
|
|
|
|
var resultJson = await plugin.ValidateTrade(12345);
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("Trade validation API unavailable"));
|
|
}
|
|
|
|
// --- ValidateCurrency tests ---
|
|
|
|
[Fact]
|
|
public async Task ValidateCurrency_ValidCode_ReturnsValid()
|
|
{
|
|
var handler = CreateOkHandler("{\"isValid\":true}");
|
|
var plugin = CreatePluginWithMocks(currencyHandler: handler);
|
|
|
|
var resultJson = await plugin.ValidateCurrency("GBP");
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.True(result.IsValid);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateCurrency_InvalidCode_ReturnsError()
|
|
{
|
|
var handler = CreateOkHandler("{\"isValid\":false,\"message\":\"Unknown currency\",\"suggestions\":[\"GBP\",\"GEL\"]}");
|
|
var plugin = CreatePluginWithMocks(currencyHandler: handler);
|
|
|
|
var resultJson = await plugin.ValidateCurrency("GBX");
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("GBP"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateCurrency_ApiError_ReturnsErrorMessage()
|
|
{
|
|
var plugin = CreatePluginWithMocks(currencyHandler: CreateErrorHandler());
|
|
|
|
var resultJson = await plugin.ValidateCurrency("USD");
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("Currency validation API unavailable"));
|
|
}
|
|
}
|