101 lines
5.6 KiB
C#
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();
|
|
}
|
|
}
|