Files
jobtrackingapp/JobTrackerApi.Tests/CvAiEvaluationTests.cs
T

101 lines
5.6 KiB
C#

using System.Net;
using System.Text;
using System.Text.Json;
using JobTrackerApi.Services;
using Microsoft.Extensions.Caching.Memory;
using Moq;
using Xunit;
namespace JobTrackerApi.Tests;
public sealed class CvAiEvaluationTests
{
[Fact]
public async Task Evaluation_uses_the_private_typed_endpoint_without_adding_profile_context()
{
var handler = new CaptureHandler();
var factory = new Mock<IHttpClientFactory>();
factory.Setup(value => value.CreateClient("ai-service")).Returns(new HttpClient(handler) { BaseAddress = new Uri("http://localhost") });
using var cache = new MemoryCache(new MemoryCacheOptions());
var service = new SummarizerService(factory.Object, cache);
var result = await service.EvaluateCvRewriteAsync("Built APIs.", "Developed APIs.", "Employer requires Kubernetes.");
Assert.Equal("/cv/evaluate-rewrite", handler.Path);
using var body = JsonDocument.Parse(handler.Body!);
Assert.Equal("Built APIs.", body.RootElement.GetProperty("candidate_source").GetString());
Assert.Equal("Developed APIs.", body.RootElement.GetProperty("proposal").GetString());
Assert.Equal("Employer requires Kubernetes.", body.RootElement.GetProperty("context_only").GetString());
Assert.Equal(3, body.RootElement.EnumerateObject().Count());
Assert.NotNull(result);
Assert.Equal("ollama", result.Provider);
Assert.False(CvAiGrounding.IsSupported("Built APIs.", "Developed APIs.", result.Text)); // Incomplete evaluator coverage is never approved.
}
[Fact]
public async Task Oversized_evaluation_never_truncates_or_calls_the_provider()
{
var factory = new Mock<IHttpClientFactory>(MockBehavior.Strict);
using var cache = new MemoryCache(new MemoryCacheOptions());
var service = new SummarizerService(factory.Object, cache);
Assert.Null(await service.EvaluateCvRewriteAsync(new string('x', 20_000), "Proposal.", null));
factory.VerifyNoOtherCalls();
}
[Fact]
public async Task Older_summarizers_fail_closed_without_an_evaluator()
=> Assert.Null(await ((ISummarizerService)new OlderSummarizer()).EvaluateCvRewriteAsync("Built APIs.", "Developed APIs.", null));
[Fact]
public async Task Typed_document_output_reaches_the_private_route_and_has_a_separate_cache_key()
{
var handler = new CaptureHandler();
var factory = new Mock<IHttpClientFactory>();
factory.Setup(value => value.CreateClient("ai-service")).Returns(new HttpClient(handler) { BaseAddress = new Uri("http://localhost") });
using var cache = new MemoryCache(new MemoryCacheOptions());
var service = new SummarizerService(factory.Object, cache);
await service.GenerateSectionWithMetadataAsync("Improve this source.", "Built APIs.");
using (var text = JsonDocument.Parse(handler.Body!)) Assert.Equal("text", text.RootElement.GetProperty("output_mode").GetString());
var document = await service.GenerateSectionWithMetadataAsync("Improve this source.", "Built APIs.", AiGenerationOutputMode.CvBlocksJson);
Assert.NotNull(document);
Assert.Equal("/cv/rewrite", handler.Path);
Assert.Equal(2, handler.RequestCount); // Identical text cannot reuse a plain-text cached result for JSON blocks.
using var body = JsonDocument.Parse(handler.Body!);
Assert.Equal("cv_blocks_json", body.RootElement.GetProperty("output_mode").GetString());
}
[Fact]
public async Task Older_implementations_keep_text_behavior_but_cannot_claim_typed_document_support()
{
ISummarizerService service = new OlderSummarizer();
Assert.Equal("Legacy rewrite.", (await service.GenerateSectionWithMetadataAsync("Improve", "Source", AiGenerationOutputMode.Text))?.Text);
Assert.Null(await service.GenerateSectionWithMetadataAsync("Improve", "Source", AiGenerationOutputMode.CvBlocksJson));
}
private sealed class CaptureHandler : HttpMessageHandler
{
public string? Path { get; private set; }
public string? Body { get; private set; }
public int RequestCount { get; private set; }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
Path = request.RequestUri!.AbsolutePath;
Body = await request.Content!.ReadAsStringAsync(ct);
RequestCount++;
var response = Path == "/cv/rewrite" ? "{\"rewritten_text\":\"Generated value.\"}"
: "{\"assessment\":{\"verdict\":\"supported\",\"coverageComplete\":true,\"claims\":[]}}";
var result = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(response, Encoding.UTF8, "application/json") };
result.Headers.Add("X-Ai-Provider", "ollama");
return result;
}
}
private sealed class OlderSummarizer : ISummarizerService
{
public Task<string?> SummarizeAsync(string text, int maxLength = 150, int minLength = 30) => Task.FromResult<string?>(null);
public Task<string?> SummarizeSectionAsync(string instruction, string text, int maxLength = 180, int minLength = 40) => Task.FromResult<string?>("Legacy rewrite.");
public Task<AiTextExtractionResult?> ExtractTextAsync(Stream stream, string fileName, string? contentType = null, CancellationToken cancellationToken = default) => Task.FromResult<AiTextExtractionResult?>(null);
public Task RunProbeAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task<AiServiceMetrics> GetMetricsAsync(CancellationToken cancellationToken = default) => throw new NotSupportedException();
}
}