using JobTrackerApi.Models; using JobTrackerApi.Services; using JobTrackerApi.Controllers; using Xunit; namespace JobTrackerApi.Tests; public class CvDocumentImportTests { [Fact] public void Grounded_duplicate_skills_keep_evidence_at_their_final_normalized_indices() { var candidate = new StructuredCvProfile { Skills = { "C#", "C#", "SQL" } }; var source = Document(("C#", "paragraph"), ("SQL", "paragraph")); var result = StructuredCvProfileJson.DeserializePersisted(StructuredCvProfileJson.SerializePersisted(CvDocumentReview.Ground(candidate, source))); Assert.Equal(new[] { "C#", "SQL" }, result.Skills); Assert.Equal("SQL", result.Metadata.Fields["skills.1"].SourceSnippet); Assert.DoesNotContain("skills.2", result.Metadata.Fields.Keys); } [Fact] public void Appended_bullet_metadata_follows_the_actual_destination_and_preserves_old_evidence() { var current = new StructuredCvProfile { Jobs = { new() { Title = "Engineer", Company = "Example", Start = "2020", End = "2021", Bullets = { "Old" } } } }; current.Metadata.Fields["jobs.0.bullets.0"] = new() { SourceSnippet = "Old" }; var incoming = new StructuredCvProfile { Jobs = { new() { Title = "Engineer", Company = "Example", Start = "2020", End = "2021", Bullets = { "New" } } } }; incoming.Metadata.Fields["jobs.0.bullets.0"] = new() { SourceSnippet = "New" }; var result = CvDocumentReview.Apply(current, incoming, new[] { new CvImportDecision("jobs.0.bullets.0", "use") }); Assert.Equal(new[] { "Old", "New" }, result.Jobs[0].Bullets); Assert.Equal("Old", result.Metadata.Fields["jobs.0.bullets.0"].SourceSnippet); Assert.Equal("New", result.Metadata.Fields["jobs.0.bullets.1"].SourceSnippet); } [Fact] public void Duplicate_imported_languages_and_jobs_do_not_create_duplicate_records() { var incoming = new StructuredCvProfile { Languages = { new() { Name = "English", Level = "B2" }, new() { Name = "English", Level = "B2" } }, Jobs = { new() { Title = "Engineer", Company = "A", Start = "2020", End = "2021" }, new() { Title = "Engineer", Company = "A", Start = "2020", End = "2021" }, new() { Title = "Engineer", Company = "A", Start = "2022", End = "2023" } } }; var decisions = CvDocumentReview.Build(new(), incoming).SelectMany(g => g.Fields).Select(f => new CvImportDecision(f.Id, "use")).ToArray(); var result = CvDocumentReview.Apply(new(), incoming, decisions); Assert.Single(result.Languages); Assert.Equal(2, result.Jobs.Count); } [Fact] public void Correcting_a_new_skill_to_an_existing_skill_keeps_metadata_on_a_real_item() { var result = CvDocumentReview.Apply(new() { Skills = { "C#" } }, new() { Skills = { "SQL" } }, new[] { new CvImportDecision("skills.0", "correct", "C#") }); var persisted = StructuredCvProfileJson.DeserializePersisted(StructuredCvProfileJson.SerializePersisted(result)); Assert.Single(persisted.Skills); Assert.Contains("skills.0", persisted.Metadata.Fields.Keys); Assert.DoesNotContain("skills.1", persisted.Metadata.Fields.Keys); } [Fact] public void Reviewed_metadata_retains_its_own_source_identity_across_later_imports() { var incoming = StructuredCvProfileJson.DeserializePersisted("""{"skills":["C#"],"metadata":{"fields":{"skills.0":{"method":"deterministic-source","sourceBlockId":"b0001","sourceArtifactId":21,"sourceExtractionRunId":7,"sourceFileSha256":"abc"}}}}"""); var merged = CvDocumentReview.Apply(new(), incoming, new[] { new CvImportDecision("skills.0", "use") }); using var json = System.Text.Json.JsonDocument.Parse(StructuredCvProfileJson.SerializePersisted(merged)); var field = json.RootElement.GetProperty("metadata").GetProperty("fields").GetProperty("skills.0"); Assert.True(field.TryGetProperty("sourceExtractionRunId", out var run)); Assert.Equal(7, run.GetInt32()); Assert.Equal(21, field.GetProperty("sourceArtifactId").GetInt32()); Assert.Equal("abc", field.GetProperty("sourceFileSha256").GetString()); } [Fact] public void Imported_contact_links_append_without_replacing_unrelated_existing_links() { var current = new StructuredCvProfile { Contact = new() { Links = { new() { Label = "Blog", Url = "https://blog.example.test" } } } }; var incoming = new StructuredCvProfile { Contact = new() { Links = { new() { Label = "Portfolio", Url = "https://portfolio.example.test" } } } }; var fields = CvDocumentReview.Build(current, incoming).SelectMany(g => g.Fields).ToList(); Assert.All(fields, f => Assert.Null(f.CurrentValue)); var result = CvDocumentReview.Apply(current, incoming, fields.Select(f => new CvImportDecision(f.Id, "use")).ToArray()); Assert.Equal(2, result.Contact.Links.Count); Assert.Equal("https://blog.example.test", result.Contact.Links[0].Url); Assert.Equal("https://portfolio.example.test", result.Contact.Links[1].Url); } private static CvImportDocument Document(params (string Text, string Kind)[] lines) => new(string.Join("\n", lines.Select(l => l.Text)), false, "application/pdf", 1, 0, "synthetic.pdf", lines.Select((l, i) => new CvImportBlock("b" + i, l.Text, l.Kind, 1)).ToArray(), Array.Empty(), "docling-native-pdf-v1"); [Fact] public void Structural_parser_preserves_labelled_norwegian_contact_and_current_year_precision() { var result = ProfileCvController.ParseImportDocument(Document(("Name: Åse Øvrebø", "paragraph"), ("Location: Tromsø", "paragraph"), ("Arbeidserfaring", "section_header"), ("Utvikler | Eksempel AS | 2020 - nå", "paragraph"), ("Bygget sikre verktøy.", "list_item"))); Assert.Equal("Åse Øvrebø", result.Contact.FullName); Assert.Equal("Tromsø", result.Contact.Location); var job = Assert.Single(result.Jobs); Assert.Equal("Eksempel AS", job.Company); Assert.Equal("2020", job.Start); Assert.Equal("nå", job.End); Assert.True(job.IsCurrent); Assert.Null(job.StartDate); Assert.Equal("Bygget sikre verktøy.", Assert.Single(job.Bullets)); } [Fact] public void Structural_parser_preserves_custom_sections_and_unknown_proficiency_without_guessing_identity() { var result = ProfileCvController.ParseImportDocument(Document(("ase.example@example.test", "paragraph"), ("Languages", "section_header"), ("Norwegian", "paragraph"), ("Courses", "section_header"), ("Database Design, Demo Academy, 2023", "paragraph"))); Assert.Null(result.Contact.FullName); Assert.Null(result.Contact.Location); Assert.Null(Assert.Single(result.Languages).Level); Assert.Equal("Database Design, Demo Academy, 2023", Assert.Single(Assert.Single(result.OtherSections).Items)); } [Fact] public void Structural_parser_keeps_unreadable_employment_as_reviewable_source_instead_of_fabricating_a_record() { var result = ProfileCvController.ParseImportDocument(Document(("Work Experience", "section_header"), ("Worked with Example Labs; dates and contractual role unavailable.", "paragraph"))); Assert.Empty(result.Jobs); Assert.Contains(result.OtherSections, section => section.Items.Contains("Worked with Example Labs; dates and contractual role unavailable.")); } [Fact] public void Review_only_applies_selected_fields_and_preserves_custom_contact_and_records() { var current = new StructuredCvProfile { Contact = new() { GitHub = "https://github.com/existing", Links = { new() { Label = "Portfolio", Url = "https://example.test" } } }, Jobs = { new() { Id = "old", Company = "Acme", Title = "Engineer", Start = "2020", End = "2021", Bullets = { "Existing" } } }, OtherSections = { new() { Title = "Private", Items = { "Preserve" } } } }; var incoming = new StructuredCvProfile { Contact = new() { Email = "new@example.test" }, Skills = { "C#", "SQL" }, Jobs = { new() { Company = "Acme", Title = "Engineer", Start = "2022", End = "2023" } } }; var groups = CvDocumentReview.Build(current, incoming); var email = groups.SelectMany(g => g.Fields).Single(f => f.Id == "contact.email"); var merged = CvDocumentReview.Apply(current, incoming, new[] { new CvImportDecision(email.Id, "correct", "correct@example.test"), new CvImportDecision("skills.0", "use") }); Assert.Equal("correct@example.test", merged.Contact.Email); Assert.Equal(current.Contact.GitHub, merged.Contact.GitHub); Assert.Single(merged.Contact.Links); Assert.Equal(new[] { "C#" }, merged.Skills); Assert.Single(merged.Jobs); Assert.Equal("old", merged.Jobs[0].Id); Assert.Single(merged.OtherSections); Assert.Contains(groups, g => g.Category == "jobs" && g.MatchDescription == "New record"); } [Theory] [InlineData("metadata.profileVersion", "use", null)] [InlineData("contact.email", "erase", null)] [InlineData("contact.email", "correct", null)] public void Invalid_decisions_are_rejected(string id, string action, string? value) { Assert.Throws(() => CvDocumentReview.Apply(new(), new() { Contact = new() { Email = "a@example.test" } }, new[] { new CvImportDecision(id, action, value) })); } [Fact] public void Grounding_removes_invented_fields_and_keeps_year_precision() { var document = new CvImportDocument("Acme\nEngineer\n2020 - 2021\nC#", false, "application/pdf", 1, 35, "cv.pdf", new[] { new CvImportBlock("b1", "Acme\nEngineer\n2020 - 2021\nC#", "text", 1) }, Array.Empty(), "docling"); var candidate = new StructuredCvProfile { Contact = new() { FullName = "Invented" }, Jobs = { new() { Company = "Acme", Title = "Engineer", Start = "2020", End = "2021", StartDate = "2020-01", Bullets = { "Invented achievement" } } } }; var result = CvDocumentReview.Ground(candidate, document); Assert.Null(result.Contact.FullName); Assert.Equal("2020", result.Jobs[0].Start); Assert.Null(result.Jobs[0].StartDate); Assert.Empty(result.Jobs[0].Bullets); Assert.Equal("b1", result.Metadata.Fields["jobs.0.company"].SourceBlockId); Assert.Null(result.Metadata.Fields["jobs.0.company"].Confidence); } }