feat(cv-import): add source-grounded review and explicit corrections

This commit is contained in:
cesnimda
2026-10-26 15:56:54 +01:00
parent 9e1726c61d
commit e941b3ccb7
24 changed files with 943 additions and 48 deletions
@@ -19,6 +19,75 @@ namespace JobTrackerApi.Tests;
public sealed class CvProcessingOperationTests
{
[Fact]
public async Task Worker_that_lost_its_lease_cannot_overwrite_a_recovered_successful_review()
{
await using var fixture = await Fixture.CreateAsync();
await fixture.SeedUserAsync();
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
fixture.DocumentExtraction = async () => { entered.TrySetResult(); await release.Task; return new("Skills\nC#", false, "application/pdf", 1, 9, "synthetic.pdf", new[] { new CvImportBlock("b1", "Skills\nC#", "paragraph", 1) }, Array.Empty<string>(), "docling-native-pdf-v1"); };
await using (var scope = fixture.Provider.CreateAsyncScope())
{
using var owner = scope.ServiceProvider.GetRequiredService<CurrentUserService>().UseBackgroundUser("user-1");
var bytes = Encoding.UTF8.GetBytes("%PDF-1.4\nSynthetic fixture");
Assert.IsType<AcceptedResult>(await CreateController(scope.ServiceProvider).Import(new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", "synthetic.pdf") { Headers = new HeaderDictionary(), ContentType = "application/pdf" }));
}
var worker = fixture.Provider.GetRequiredService<AiOperationWorker>().RunOnceAsync(default);
try
{
await entered.Task.WaitAsync(TimeSpan.FromSeconds(10));
await using var recovered = fixture.Provider.CreateAsyncScope();
var db = recovered.ServiceProvider.GetRequiredService<JobTrackerContext>();
var now = DateTime.UtcNow;
await db.UserOperations.IgnoreQueryFilters().ExecuteUpdateAsync(s => s.SetProperty(o => o.Status, OperationStatuses.Succeeded).SetProperty(o => o.LeaseToken, (string?)null).SetProperty(o => o.CompletedAtUtc, now));
await db.CvExtractionRuns.IgnoreQueryFilters().ExecuteUpdateAsync(s => s.SetProperty(r => r.Status, "pending_review").SetProperty(r => r.StructuredProfileJson, "{\"skills\":[\"Recovered winner\"]}"));
}
finally { release.TrySetResult(); }
Assert.True(await worker.WaitAsync(TimeSpan.FromSeconds(10)));
await using var verify = fixture.Provider.CreateAsyncScope();
var winner = await verify.ServiceProvider.GetRequiredService<JobTrackerContext>().CvExtractionRuns.IgnoreQueryFilters().SingleAsync();
Assert.Equal("pending_review", winner.Status);
Assert.Contains("Recovered winner", winner.StructuredProfileJson);
}
[Fact]
public async Task Document_failure_message_is_bounded_and_does_not_crash_the_worker_or_disclose_source()
{
await using var fixture = await Fixture.CreateAsync();
await fixture.SeedUserAsync();
fixture.ExtractionFailure = new InvalidOperationException("PRIVATE_SOURCE " + new string('x', 2000));
await using (var scope = fixture.Provider.CreateAsyncScope())
{
using var owner = scope.ServiceProvider.GetRequiredService<CurrentUserService>().UseBackgroundUser("user-1");
var bytes = Encoding.UTF8.GetBytes("%PDF-1.4\nSynthetic fixture");
Assert.IsType<AcceptedResult>(await CreateController(scope.ServiceProvider).Import(new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", "synthetic.pdf") { Headers = new HeaderDictionary(), ContentType = "application/pdf" }));
}
Assert.True(await fixture.Provider.GetRequiredService<AiOperationWorker>().RunOnceAsync(default));
await using var verify = fixture.Provider.CreateAsyncScope();
var operation = await verify.ServiceProvider.GetRequiredService<JobTrackerContext>().UserOperations.IgnoreQueryFilters().SingleAsync();
Assert.Equal(OperationStatuses.Failed, operation.Status);
Assert.InRange(operation.FailureMessage!.Length, 1, 512);
Assert.DoesNotContain("PRIVATE_SOURCE", operation.FailureMessage);
}
[Fact]
public async Task Structured_document_worker_publishes_review_under_its_real_database_lease()
{
await using var fixture = await Fixture.CreateAsync();
await fixture.SeedUserAsync();
CvProcessingOperationResponse response;
await using (var scope = fixture.Provider.CreateAsyncScope())
{
using var owner = scope.ServiceProvider.GetRequiredService<CurrentUserService>().UseBackgroundUser("user-1");
var bytes = Encoding.UTF8.GetBytes("%PDF-1.4\nSynthetic transport fixture");
response = Response(await CreateController(scope.ServiceProvider).Import(new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", "synthetic.pdf") { Headers = new HeaderDictionary(), ContentType = "application/pdf" }));
}
Assert.True(await fixture.Provider.GetRequiredService<AiOperationWorker>().RunOnceAsync(default));
await using var verify = fixture.Provider.CreateAsyncScope();
var db = verify.ServiceProvider.GetRequiredService<JobTrackerContext>();
Assert.Equal("pending_review", (await db.CvExtractionRuns.IgnoreQueryFilters().SingleAsync()).Status);
Assert.Equal(OperationStatuses.Succeeded, (await db.UserOperations.IgnoreQueryFilters().SingleAsync()).Status);
Assert.Empty(await db.CareerProfiles.IgnoreQueryFilters().ToListAsync());
}
[Fact]
public async Task Cancellation_of_an_expired_import_operation_cannot_resurrect_its_review()
{
@@ -227,6 +296,8 @@ public sealed class CvProcessingOperationTests
private readonly string _tempRoot;
public ServiceProvider Provider { get; }
public AiGenerationException? GenerationFailure { get; set; }
public Exception? ExtractionFailure { get; set; }
public Func<Task<CvImportDocument>>? DocumentExtraction { get; set; }
private Fixture(SqliteConnection connection, string tempRoot, ServiceProvider provider)
{
@@ -251,6 +322,8 @@ public sealed class CvProcessingOperationTests
environment.SetupGet(item => item.ContentRootPath).Returns(tempRoot);
Fixture? fixture = null;
var summarizer = new Mock<ISummarizerService>();
summarizer.Setup(item => item.ExtractCvDocumentAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.Returns(async () => fixture?.ExtractionFailure is not null ? throw fixture.ExtractionFailure : fixture?.DocumentExtraction is not null ? await fixture.DocumentExtraction() : new CvImportDocument("Skills\nC#", false, "application/pdf", 1, 9, "synthetic.pdf", new[] { new CvImportBlock("b1", "Skills", "section_header", 1), new CvImportBlock("b2", "C#", "paragraph", 1) }, Array.Empty<string>(), "docling-native-pdf-v1"));
summarizer.Setup(item => item.ExtractTextAsync(
It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AiTextExtractionResult(