fix: preserve blank CVs and reliable editor undo

This commit is contained in:
cesnimda
2026-10-27 07:37:03 +01:00
parent 5d4b8f23ce
commit 663023a088
22 changed files with 950 additions and 101 deletions
@@ -364,6 +364,20 @@ public sealed class ApplicationIntelligenceTests
Assert.Empty(match.RelevantExperience);
}
[Fact]
public async Task Match_does_not_inherit_experience_from_an_explicit_blank_cv()
{
var (db, intelligence, _) = New("user-1");
await using var dispose = db;
var job = await SeedJobAsync(db, "user-1");
await SeedProfileAsync(db, "user-1");
await AttachCvAsync(db, "user-1", job.Id, CvVariantSettingsJson.Deserialize("{\"sectionsExplicit\":true,\"includeProfileContact\":false,\"sections\":[]}"));
var match = await intelligence.MatchAsync("user-1", job.Id, default);
Assert.True(match!.HasSelectedCv);
Assert.Empty(match.RelevantExperience);
Assert.Empty(match.RelevantProjects);
}
[Fact]
public async Task Match_is_not_readable_for_another_users_application()
{
+14
View File
@@ -19,6 +19,20 @@ public sealed class CvAiReadinessTests
{
private const string Source = "Built and maintained internal web applications.";
private const string Paraphrase = "Developed and maintained internal web applications.";
[Fact]
public async Task Explicit_blank_document_AI_does_not_send_unselected_profile_facts()
{
await using var fixture = new Fixture();
await fixture.Open();
await fixture.Variants.SaveAsync("owner", fixture.Cv.Id, null,
CvVariantSettingsJson.Deserialize("{\"sectionsExplicit\":true,\"includeProfileContact\":false,\"sections\":[]}"), "manual", 1, default);
var error = await Assert.ThrowsAsync<CvAiSuggestionException>(() => fixture.Suggestions.GenerateAsync(
"owner", fixture.Cv.Id, 2, null, "document-improve", null, null, "Rewrite", default));
Assert.Equal("cv_ai_source_empty", error.Code);
fixture.Ai.VerifyNoOtherCalls();
}
private sealed class Fixture : IAsyncDisposable
{
public readonly SqliteConnection Connection = new("Data Source=:memory:");
+66
View File
@@ -28,6 +28,72 @@ public sealed class CvBuilderTests
// ---- Resolver (the lens over the master profile) ----
[Fact]
public void Explicit_blank_roundtrips_without_inheriting_profile_content_or_header()
{
var settings = CvVariantSettingsJson.Deserialize("{\"sectionsExplicit\":true,\"includeProfileContact\":false,\"sections\":[],\"showPhoto\":true}");
var json = CvVariantSettingsJson.Serialize(settings);
Assert.Contains("\"sectionsExplicit\":true", json);
Assert.Contains("\"includeProfileContact\":false", json);
var model = CvVariantResolver.Build(Rich(), CvVariantSettingsJson.Deserialize(json), "Fallback", "data:image/png;base64,AA==");
Assert.Empty(model.Sections);
Assert.Empty(model.FullName);
Assert.Null(model.Headline);
Assert.Empty(model.Contact);
Assert.Null(model.PhotoDataUrl);
foreach (var theme in CvThemeCatalog.Themes)
{
var html = new ThemedCvRenderer().Render(model, theme, settings).Html;
Assert.DoesNotContain("<header", html);
Assert.DoesNotContain("Ada Lovelace", html);
Assert.DoesNotContain("Fallback", html);
}
}
[Fact]
public void Explicit_sections_include_only_selected_content_and_allow_authored_headline()
{
var settings = CvVariantSettingsJson.Deserialize("{\"sectionsExplicit\":true,\"includeProfileContact\":false,\"headline\":\"Authored headline\",\"sections\":[{\"key\":\"skills\"}]}");
var model = CvVariantResolver.Build(Rich(), settings, "Fallback", null);
Assert.Equal("skills", Assert.Single(model.Sections).Key);
Assert.Equal("Authored headline", model.Headline);
Assert.Empty(model.FullName);
Assert.Empty(model.Contact);
}
[Fact]
public void Legacy_inclusion_flags_remain_absent_and_preserve_profile_inheritance()
{
var settings = CvVariantSettingsJson.Deserialize("{\"sections\":[]}");
var json = CvVariantSettingsJson.Serialize(settings);
Assert.DoesNotContain("sectionsExplicit", json);
Assert.DoesNotContain("includeProfileContact", json);
var model = CvVariantResolver.Build(Rich(), settings, "Fallback", null);
Assert.Equal("Ada Lovelace", model.FullName);
Assert.NotEmpty(model.Sections);
Assert.NotEmpty(model.Contact);
}
[Fact]
public async Task Explicit_blank_survives_save_duplicate_restore_and_design_change()
{
var (db, service) = NewService("user-1");
await using var dispose = db;
var settings = CvVariantSettingsJson.Deserialize("{\"sectionsExplicit\":true,\"includeProfileContact\":false,\"sections\":[]}");
var created = await service.CreateAsync("user-1", "Blank", null, settings, default);
settings.ThemeId = "clean";
await service.SaveAsync("user-1", created.Id, null, settings, "manual", 1, default);
var copy = await service.DuplicateAsync("user-1", created.Id, null, default);
var restored = await service.RestoreVersionAsync("user-1", created.Id, 1, 2, default);
foreach (var document in new[] { copy!, restored! })
{
var stored = CvVariantSettingsJson.Deserialize(document.SettingsJson);
Assert.Contains("\"sectionsExplicit\":true", document.SettingsJson);
Assert.Contains("\"includeProfileContact\":false", document.SettingsJson);
Assert.Empty(CvVariantResolver.Build(Rich(), stored, "Fallback", null).Sections);
}
}
[Fact]
public void Resolver_defaults_include_every_populated_section()
{
@@ -16,6 +16,27 @@ namespace JobTrackerApi.Tests;
public sealed class CvExportRevisionTests
{
[Fact]
public async Task Explicit_blank_export_is_rejected_before_pdf_allocation()
{
var current = new Mock<ICurrentUserService>();
current.SetupGet(x => x.UserId).Returns("owner");
await using var db = new JobTrackerContext(new DbContextOptionsBuilder<JobTrackerContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options, current.Object);
var variants = new CvVariantService(db, new CareerProfileService(db), new ThemedCvRenderer());
var settings = CvVariantSettingsJson.Deserialize("{\"sectionsExplicit\":true,\"includeProfileContact\":false,\"sections\":[]}");
var cv = await variants.CreateAsync("owner", "Blank", null, settings, default);
var user = new ApplicationUser { Id = "owner", FirstName = "Inherited name" };
var users = TestHostFactory.CreateUserManager(user);
users.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
var pdf = new Mock<ICvPdfExporter>(MockBehavior.Strict);
var controller = new CvVariantController(users.Object, variants, pdf.Object, Mock.Of<ISummarizerService>())
{ ControllerContext = new() { HttpContext = new DefaultHttpContext() } };
var result = Assert.IsType<BadRequestObjectResult>(await controller.ExportPdf(cv.Id, default, 1));
Assert.Equal("cv_empty_content", JsonSerializer.SerializeToElement(result.Value).GetProperty("code").GetString());
pdf.VerifyNoOtherCalls();
}
[Theory]
[InlineData(1, "owner", 409)]
[InlineData(0, "owner", 409)]