feat(cv)!: queue durable processing
CV upload now returns 202 with an owner-scoped operation instead of holding the request through parsing. Existing review approval remains required. BREAKING CHANGE: profile-cv upload responses use the durable operation contract.
This commit is contained in:
@@ -45,6 +45,7 @@ public sealed class ProfileCvControllerTests
|
||||
var user = new ApplicationUser { Id = "user-1" };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
ConfigureWorkerUser(userManager, user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService
|
||||
@@ -76,9 +77,9 @@ public sealed class ProfileCvControllerTests
|
||||
ContentType = "text/markdown"
|
||||
};
|
||||
|
||||
var result = await controller.Upload(file);
|
||||
var result = await UploadAndProcessAsync(controller, db, file);
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
Assert.IsType<AcceptedResult>(result);
|
||||
var artifact = await db.CvUploadArtifacts.SingleAsync();
|
||||
var run = await db.CvExtractionRuns.SingleAsync();
|
||||
Assert.Equal("user-1", artifact.OwnerUserId);
|
||||
@@ -235,10 +236,10 @@ public sealed class ProfileCvControllerTests
|
||||
userManager.Setup(x => x.FindByIdAsync(user.Id)).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.GetRolesAsync(user)).ReturnsAsync(new[] { "Premium" });
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService.Setup(x => x.SummarizeSectionAsync(
|
||||
aiService.Setup(x => x.GenerateSectionWithMetadataAsync(
|
||||
It.Is<string>(instruction => instruction.StartsWith("Rewrite this CV", StringComparison.Ordinal)),
|
||||
It.IsAny<string>(), 1800, 500))
|
||||
.ReturnsAsync(user.ProfileCvText);
|
||||
It.IsAny<string>(), 1800, 500, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AiGenerationResult(user.ProfileCvText));
|
||||
aiService.Setup(x => x.SummarizeSectionAsync(
|
||||
It.Is<string>(instruction => instruction.Contains("Extract this CV into structured JSON", StringComparison.Ordinal)),
|
||||
It.IsAny<string>(), 3200, 900))
|
||||
@@ -375,6 +376,7 @@ public sealed class ProfileCvControllerTests
|
||||
var user = new ApplicationUser { Id = "user-1" };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
ConfigureWorkerUser(userManager, user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService
|
||||
@@ -398,9 +400,9 @@ public sealed class ProfileCvControllerTests
|
||||
ContentType = "application/pdf"
|
||||
};
|
||||
|
||||
var result = await controller.Upload(file);
|
||||
var result = await UploadAndProcessAsync(controller, db, file);
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
Assert.IsType<AcceptedResult>(result);
|
||||
var savedRun = await db.CvExtractionRuns.SingleAsync();
|
||||
Assert.Equal(reconstructed, savedRun.NormalizedText);
|
||||
|
||||
@@ -425,6 +427,7 @@ public sealed class ProfileCvControllerTests
|
||||
var user = new ApplicationUser { Id = "user-1" };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
ConfigureWorkerUser(userManager, user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService
|
||||
@@ -456,9 +459,9 @@ public sealed class ProfileCvControllerTests
|
||||
ContentType = "application/pdf"
|
||||
};
|
||||
|
||||
var result = await controller.Upload(file);
|
||||
var result = await UploadAndProcessAsync(controller, db, file);
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
Assert.IsType<AcceptedResult>(result);
|
||||
normalizer.Verify(x => x.NormalizeAsync(It.Is<string>(text => text.Contains("Warwickshire County Council", StringComparison.Ordinal)), It.IsAny<CancellationToken>()), Times.Once);
|
||||
var savedRun = await db.CvExtractionRuns.SingleAsync();
|
||||
var structured = StructuredCvProfileJson.Deserialize(savedRun.StructuredProfileJson);
|
||||
@@ -475,6 +478,7 @@ public sealed class ProfileCvControllerTests
|
||||
var user = new ApplicationUser { Id = "user-1" };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
ConfigureWorkerUser(userManager, user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService
|
||||
@@ -498,9 +502,9 @@ public sealed class ProfileCvControllerTests
|
||||
ContentType = "application/pdf"
|
||||
};
|
||||
|
||||
var result = await controller.Upload(file);
|
||||
var result = await UploadAndProcessAsync(controller, db, file);
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
Assert.IsType<AcceptedResult>(result);
|
||||
var savedRun = await db.CvExtractionRuns.SingleAsync();
|
||||
var structured = StructuredCvProfileJson.Deserialize(savedRun.StructuredProfileJson);
|
||||
Assert.Equal("Connor Babbington", structured.Contact.FullName);
|
||||
@@ -1068,6 +1072,7 @@ public sealed class ProfileCvControllerTests
|
||||
var user = new ApplicationUser { Id = "user-1" };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
ConfigureWorkerUser(userManager, user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService
|
||||
@@ -1098,9 +1103,9 @@ public sealed class ProfileCvControllerTests
|
||||
Headers = new HeaderDictionary(),
|
||||
ContentType = "text/markdown"
|
||||
};
|
||||
var result = await controller.Upload(file);
|
||||
var result = await UploadAndProcessAsync(controller, db, file);
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
Assert.IsType<AcceptedResult>(result);
|
||||
var run = await db.CvExtractionRuns.SingleAsync();
|
||||
Assert.Contains("Built APIs", run.NormalizedText);
|
||||
Assert.Equal("Connor Babbington", StructuredCvProfileJson.Deserialize(run.StructuredProfileJson).Contact.FullName);
|
||||
@@ -1346,6 +1351,22 @@ public sealed class ProfileCvControllerTests
|
||||
return StructuredCvProfileJson.Normalize((StructuredCvProfile)result!);
|
||||
}
|
||||
|
||||
private static async Task<IActionResult> UploadAndProcessAsync(ProfileCvController controller, JobTrackerContext db, IFormFile file)
|
||||
{
|
||||
var accepted = Assert.IsType<AcceptedResult>(await controller.Upload(file));
|
||||
var run = await db.CvExtractionRuns.SingleAsync();
|
||||
var outcome = Assert.IsType<CvProcessingOutcome>(await controller.ProcessQueuedRunAsync(run.Id, CancellationToken.None));
|
||||
Assert.True(outcome.Succeeded, outcome.FailureMessage);
|
||||
return accepted;
|
||||
}
|
||||
|
||||
private static void ConfigureWorkerUser(Mock<UserManager<ApplicationUser>> userManager, ApplicationUser user)
|
||||
{
|
||||
user.AiEnabled = true;
|
||||
userManager.Setup(x => x.FindByIdAsync(user.Id)).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.GetRolesAsync(user)).ReturnsAsync(new[] { "Premium" });
|
||||
}
|
||||
|
||||
private static ProfileCvController CreateController(UserManager<ApplicationUser> userManager, ISummarizerService aiService, JobTrackerContext db, AppPaths paths, ICvAiClassifier? cvAiClassifier = null, ICvAiNormalizer? cvAiNormalizer = null)
|
||||
{
|
||||
return new ProfileCvController(userManager, aiService, db, paths, null, cvAiClassifier ?? NoOpCvAiClassifier.Instance, cvAiNormalizer ?? NoOpCvAiNormalizer.Instance)
|
||||
|
||||
Reference in New Issue
Block a user