Harden CV rewrite diagnostics and preview PDFs

This commit is contained in:
2026-04-11 21:36:45 +02:00
parent fcccecefa3
commit 534534b333
4 changed files with 371 additions and 48 deletions
@@ -556,6 +556,129 @@ public sealed class ProfileCvControllerTests
Assert.Equal("Warwickshire College, UK", structured.Education[0].Location);
}
[Fact]
public async Task Rewrite_section_returns_ai_service_unavailable_detail_when_ai_health_is_unhealthy()
{
var user = new ApplicationUser { Id = "user-1", ProfileCvText = "Professional Summary\nBuilt backend systems." };
var userManager = CreateUserManager();
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
var aiService = new Mock<ISummarizerService>();
aiService
.Setup(x => x.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), 1800, 400))
.ReturnsAsync(string.Empty);
aiService
.Setup(x => x.GetMetricsAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new AiServiceMetrics(
Healthy: false,
Model: "distilbart",
Device: "cpu",
GpuAvailable: false,
GpuName: null,
OcrAvailable: true,
OcrLanguages: "eng",
OllamaConfigured: true,
OllamaReachable: true,
OllamaModel: "qwen2.5:7b",
OllamaModelAvailable: true,
OllamaVersion: "0.6.0",
OllamaInstalledModels: new List<string> { "qwen2.5:7b" },
OllamaLoadedModels: new List<string>(),
OllamaLoadedCount: 0,
HealthLatencyMs: 21,
ProbeLatencyMs: null,
LastProbeAt: null,
LastProbeSuccessAt: null,
LastProbeFailureAt: null,
ProbeFailures: 1,
Requests: 1,
CacheHits: 0,
CacheMisses: 1,
Failures: 1,
AverageLatencyMs: 21,
OcrRequests: 0,
OcrFailures: 0,
AverageOcrLatencyMs: null,
LastOcrSuccessAt: null,
LastOcrFailureAt: null,
LastSuccessAt: null,
LastFailureAt: DateTimeOffset.UtcNow,
LastError: "Model loading is disabled by AI_SERVICE_SKIP_MODEL_LOAD."));
await using var db = CreateDb();
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
var result = await controller.RewriteSection(new ProfileCvController.RewriteSectionRequest());
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(StatusCodes.Status502BadGateway, objectResult.StatusCode);
var payload = Assert.IsType<ProfileCvController.CvRewriteFailureDto>(objectResult.Value);
Assert.Equal("ai-service-unavailable", payload.Code);
Assert.Contains("could not rewrite", payload.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("unavailable", payload.Detail ?? string.Empty, StringComparison.OrdinalIgnoreCase);
Assert.Contains("AI_SERVICE_SKIP_MODEL_LOAD", payload.LastAiError ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Rewrite_section_returns_rewrite_empty_detail_when_ai_health_is_healthy()
{
var user = new ApplicationUser { Id = "user-1", ProfileCvText = "Professional Summary\nBuilt backend systems." };
var userManager = CreateUserManager();
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
var aiService = new Mock<ISummarizerService>();
aiService
.Setup(x => x.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), 1800, 400))
.ReturnsAsync(string.Empty);
aiService
.Setup(x => x.GetMetricsAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new AiServiceMetrics(
Healthy: true,
Model: "distilbart",
Device: "cpu",
GpuAvailable: false,
GpuName: null,
OcrAvailable: true,
OcrLanguages: "eng",
OllamaConfigured: true,
OllamaReachable: true,
OllamaModel: "qwen2.5:7b",
OllamaModelAvailable: true,
OllamaVersion: "0.6.0",
OllamaInstalledModels: new List<string> { "qwen2.5:7b" },
OllamaLoadedModels: new List<string>(),
OllamaLoadedCount: 0,
HealthLatencyMs: 21,
ProbeLatencyMs: null,
LastProbeAt: null,
LastProbeSuccessAt: null,
LastProbeFailureAt: null,
ProbeFailures: 0,
Requests: 1,
CacheHits: 0,
CacheMisses: 1,
Failures: 0,
AverageLatencyMs: 21,
OcrRequests: 0,
OcrFailures: 0,
AverageOcrLatencyMs: null,
LastOcrSuccessAt: null,
LastOcrFailureAt: null,
LastSuccessAt: DateTimeOffset.UtcNow,
LastFailureAt: null,
LastError: null));
await using var db = CreateDb();
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
var result = await controller.RewriteSection(new ProfileCvController.RewriteSectionRequest());
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(StatusCodes.Status502BadGateway, objectResult.StatusCode);
var payload = Assert.IsType<ProfileCvController.CvRewriteFailureDto>(objectResult.Value);
Assert.Equal("rewrite-empty", payload.Code);
Assert.Contains("empty", payload.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("no usable text", payload.Detail ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Rewrite_section_can_target_saved_job_context_and_whole_cv()
{