using JobTrackerApi.Models; using JobTrackerApi.Services; using Xunit; namespace JobTrackerApi.Tests; public class CvImportMappingTests { private static readonly CvImportDocument Source = new("Engineer Example 2020\nDesigner Other 2022\nPortfolio https://example.test\nSQL", false, "application/pdf", 2, 100, "synthetic.pdf", new[] { new CvImportBlock("b1", "Engineer Example 2020", "paragraph", 1), new CvImportBlock("b2", "Designer Other 2022", "paragraph", 2), new CvImportBlock("b3", "Portfolio https://example.test", "paragraph", 2), new CvImportBlock("b4", "SQL", "paragraph", 2) }, Array.Empty(), "docling"); private static CvImportMappedField Field(string name, string value, string block = "b1", bool corrected = false) => new(name, value, block, corrected); private static StructuredCvProfile Apply(StructuredCvProfile current, params CvImportRecordMapping[] mappings) => CvImportMapping.Apply(current, mappings, Source, 21, 7, "abc"); [Fact] public void Mapping_only_authors_multiple_missing_records_with_real_per_field_origin_and_date_precision() { var result = Apply(new(), new("first", "jobs", null, new[] { Field("title", "Engineer"), Field("company", "Example"), Field("start", "2020") }), new("second", "jobs", null, new[] { Field("title", "Designer", "b2"), Field("company", "Other", "b2"), Field("start", "2022", "b2") })); Assert.Equal(2, result.Jobs.Count); Assert.Equal("2020", result.Jobs[0].Start); Assert.Null(result.Jobs[0].End); Assert.Null(result.Jobs[0].StartDate); var evidence = result.Metadata.Fields["jobs.1.company"]; Assert.Equal("b2", evidence.SourceBlockId); Assert.Equal(2, evidence.SourcePage); Assert.Equal(21, evidence.SourceArtifactId); Assert.Equal(7, evidence.SourceExtractionRunId); Assert.Equal("abc", evidence.SourceFileSha256); Assert.Equal("user-source-mapping", evidence.Method); } [Fact] public void Explicit_target_updates_only_approved_leaves_and_lists_append_with_destination_evidence() { var current = new StructuredCvProfile { Jobs = { new() { Id = "stable", Title = "Original", Company = "Keep company", Bullets = { "Old" } } }, Skills = { "C#" } }; current.Metadata.Fields["jobs.0.bullets.0"] = new() { SourceSnippet = "Old" }; var result = Apply(current, new("target", "jobs", 0, new[] { Field("title", "Engineer"), Field("bullets", "Reviewed achievement", corrected: true) }), new("skill", "skills", null, new[] { Field("value", "SQL", "b4") })); var job = Assert.Single(result.Jobs); Assert.Equal("stable", job.Id); Assert.Equal("Keep company", job.Company); Assert.Equal("Engineer", job.Title); Assert.Equal(new[] { "Old", "Reviewed achievement" }, job.Bullets); Assert.Equal("Old", result.Metadata.Fields["jobs.0.bullets.0"].SourceSnippet); Assert.Equal("user-correction", result.Metadata.Fields["jobs.0.bullets.1"].Method); Assert.Equal(new[] { "C#", "SQL" }, result.Skills); Assert.Equal("Original", current.Jobs[0].Title); } [Fact] public void Contact_mapping_authors_labelled_url_without_replacing_saved_links() { var result = Apply(new() { Contact = new() { GitHub = "https://github.com/saved", Links = { new() { Label = "Blog", Url = "https://blog.test" } } } }, new CvImportRecordMapping("link", "contact", null, new[] { Field("linkLabel", "Portfolio", "b3"), Field("linkUrl", "https://example.test", "b3") })); Assert.Equal("https://github.com/saved", result.Contact.GitHub); Assert.Equal(2, result.Contact.Links.Count); Assert.Equal("Portfolio", result.Contact.Links[1].Label); Assert.Equal("b3", result.Metadata.Fields["contact.links.1.url"].SourceBlockId); } [Theory] [InlineData("metadata", "value", "Engineer", "b1", null)] [InlineData("jobs", "id", "Engineer", "b1", null)] [InlineData("jobs", "startDate", "2020", "b1", null)] [InlineData("jobs", "title.extra", "Engineer", "b1", null)] [InlineData("skills", "value.sourceBlockId", "SQL", "b4", null)] [InlineData("skills", "sourceBlockId", "SQL", "b4", null)] [InlineData("jobs", "title", "Engineer", "foreign-block", null)] [InlineData("jobs", "title", "Not in source", "b1", null)] [InlineData("jobs", "isCurrent", "yes", "b1", null)] [InlineData("jobs", "title", "Engineer", "b1", 0)] [InlineData("jobs", "title", "Engineer", "b1", -1)] [InlineData("skills", "value", "SQL", "b4", 0)] public void Invalid_or_unowned_mappings_fail_closed(string category, string name, string value, string block, int? index) => Assert.Throws(() => Apply(new(), new CvImportRecordMapping("invalid", category, index, new[] { Field(name, value, block) }))); [Fact] public void Empty_identity_duplicate_ids_fields_and_oversized_requests_are_rejected() { Assert.Throws(() => Apply(new(), new CvImportRecordMapping("empty", "jobs", null, Array.Empty()))); Assert.Throws(() => Apply(new(), new CvImportRecordMapping("identity", "jobs", null, new[] { Field("start", "2020") }))); var mapping = new CvImportRecordMapping("same", "jobs", null, new[] { Field("title", "Engineer") }); Assert.Throws(() => Apply(new(), mapping, mapping)); Assert.Throws(() => Apply(new(), mapping with { Fields = new[] { Field("title", "Engineer"), Field("title", "Engineer") } })); Assert.Throws(() => Apply(new(), mapping with { Fields = new[] { Field("title", new string('x', 8001), corrected: true) } })); Assert.Throws(() => Apply(new(), Enumerable.Range(0, 26).Select(i => mapping with { ClientId = i.ToString() }).ToArray())); Assert.Throws(() => Apply(new(), mapping with { Fields = Enumerable.Repeat(Field("bullets", "Engineer"), 201).ToArray() })); } [Fact] public void Grounding_respects_valid_selected_occurrence_and_rejects_invalid_selected_evidence() { var document = Source with { Blocks = new[] { new CvImportBlock("first", "Engineer", "paragraph", 1), new CvImportBlock("second", "Engineer Example", "paragraph", 2) } }; var candidate = new StructuredCvProfile { Jobs = { new() { Title = "Engineer", Company = "Example" } } }; candidate.Metadata.Fields["jobs.0.title"] = new() { SourceBlockId = "second", SourceSnippet = "Engineer", SourcePage = 2 }; candidate.Metadata.Fields["jobs.0.company"] = new() { SourceBlockId = "first", SourceSnippet = "Example" }; var result = CvDocumentReview.Ground(candidate, document); Assert.Equal("second", result.Metadata.Fields["jobs.0.title"].SourceBlockId); Assert.Null(result.Jobs[0].Company); } [Fact] public void Selected_evidence_survives_scalar_list_deduplication_without_changing_occurrence() { var source = Source with { Blocks = new[] { new CvImportBlock("first", "SQL", "paragraph", 1), new CvImportBlock("second", "SQL Engineer", "paragraph", 2) } }; var candidate = new StructuredCvProfile { Skills = { "SQL", "SQL", "Engineer" } }; candidate.Metadata.Fields["skills.2"] = new() { SourceBlockId = "second", SourceSnippet = "Engineer" }; var result = CvDocumentReview.Ground(candidate, source); Assert.Equal(new[] { "SQL", "Engineer" }, result.Skills); Assert.Equal("second", result.Metadata.Fields["skills.1"].SourceBlockId); Assert.DoesNotContain("skills.2", result.Metadata.Fields.Keys); } [Fact] public void Mapping_comparison_catalog_exposes_only_allowlisted_current_values_for_the_selected_target() { var current = new StructuredCvProfile { Contact = new() { FullName = "Saved name", GitHub = "https://github.com/saved" }, Skills = { "C#", "SQL" }, Jobs = { new() { Id = "private-id", Title = "Saved title", Company = "Saved company", StartDate = "2020-01", Bullets = { "First", "Second" } }, new() { Title = "Different title" } }, }; current.Metadata.Fields["jobs.0.title"] = new() { SourceSnippet = "private evidence" }; var targets = CvImportMapping.Targets(current); Assert.DoesNotContain(targets, t => t.Category == "contact"); var target = Assert.Single(targets, t => t.Category == "jobs" && t.Index == 0); Assert.Equal("Saved title", target.Values!["title"]); Assert.Equal("First\nSecond", target.Values["bullets"]); Assert.DoesNotContain("id", target.Values.Keys); Assert.DoesNotContain("metadata", target.Values.Keys); Assert.DoesNotContain("startDate", target.Values.Keys); Assert.Equal("Different title", targets.Single(t => t.Category == "jobs" && t.Index == 1).Values!["title"]); var catalog = CvImportMapping.Catalog(current); Assert.Equal("Saved name", catalog.Single(c => c.Category == "contact").Fields.Single(f => f.Name == "fullName").CurrentValue); Assert.Equal("C#\nSQL", catalog.Single(c => c.Category == "skills").Fields.Single(f => f.Name == "value").CurrentValue); Assert.All(catalog.Single(c => c.Category == "jobs").Fields, f => Assert.Null(f.CurrentValue)); } [Theory] [InlineData("Py", "Python", false)] [InlineData("2020", "12020", false)] [InlineData("SQL", "NoSQL", false)] [InlineData("C#", "Used C# and SQL.", true)] [InlineData(".NET", "Built .NET services.", true)] [InlineData("2020", "2020 - 2021", true)] [InlineData("Engineer", "Engineer.", true)] public void Source_mode_mapping_requires_honest_token_boundaries(string value, string source, bool supported) { var document = Source with { Blocks = new[] { new CvImportBlock("token", source, "paragraph", 1) } }; var mapping = new CvImportRecordMapping("token", "skills", null, new[] { Field("value", value, "token") }); if (supported) Assert.Equal(value, Assert.Single(CvImportMapping.Apply(new(), new[] { mapping }, document, 1, 1, "hash").Skills)); else Assert.Throws(() => CvImportMapping.Apply(new(), new[] { mapping }, document, 1, 1, "hash")); var correction = mapping with { Fields = new[] { Field("value", value, "token", corrected: true) } }; Assert.Equal(value, Assert.Single(CvImportMapping.Apply(new(), new[] { correction }, document, 1, 1, "hash").Skills)); } }