Files
jobtrackingapp/JobTrackerApi.Tests/CvMultilineImportTests.cs
T

300 lines
16 KiB
C#

using JobTrackerApi.Controllers;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Xunit;
namespace JobTrackerApi.Tests;
public class CvMultilineImportTests
{
private static CvImportDocument Document(params (string Text, string Kind)[] blocks) => new(
string.Join("\n", blocks.Select(b => b.Text)), false, "application/pdf", Math.Max(1, (blocks.Length + 3) / 4), 0, "synthetic.pdf",
blocks.Select((b, i) => new CvImportBlock($"b{i + 1:0000}", b.Text, b.Kind, i / 4 + 1)).ToArray(), [], "docling-native-pdf-v1");
private static StructuredCvProfile Parse(params (string Text, string Kind)[] blocks) => ProfileCvController.ParseImportDocument(Document(blocks));
[Fact]
public void Labelled_jobs_support_both_orders_dates_location_and_technologies()
{
var result = Parse(("Work Experience", "section_header"), ("Employer: Example AS", "paragraph"),
("Title: Engineer", "paragraph"), ("2020 - 2021", "paragraph"), ("Location: Oslo", "paragraph"),
("Technologies: C#; SQL", "paragraph"), ("Built tools.", "list_item"),
("Title: Lead", "paragraph"), ("Company: Other AS", "paragraph"), ("Dates: 2022 - Present", "paragraph"));
Assert.Equal(2, result.Jobs.Count);
Assert.Equal("Example AS", result.Jobs[0].Company);
Assert.Equal("Engineer", result.Jobs[0].Title);
Assert.Equal("2020", result.Jobs[0].Start);
Assert.Equal("2021", result.Jobs[0].End);
Assert.Equal("Oslo", result.Jobs[0].Location);
Assert.Equal(new[] { "C#", "SQL" }, result.Jobs[0].Skills);
Assert.Equal("Built tools.", Assert.Single(result.Jobs[0].Bullets));
Assert.Equal("Other AS", result.Jobs[1].Company);
Assert.True(result.Jobs[1].IsCurrent);
Assert.Null(result.Jobs[0].StartDate);
}
[Fact]
public void Valid_job_does_not_absorb_an_unlabelled_next_record_or_its_bullets()
{
var result = Parse(("Work Experience", "section_header"), ("Engineer | Example AS | 2020 - 2021", "paragraph"),
("- Built tools.", "paragraph"), ("Other AS", "paragraph"), ("Manager", "paragraph"),
("2022 - Present", "paragraph"), ("Managed releases.", "list_item"));
Assert.Equal(new[] { "Built tools." }, Assert.Single(result.Jobs).Bullets);
Assert.Equal(new[] { "Other AS", "Manager", "2022 - Present", "Managed releases." }, result.OtherSections.SelectMany(s => s.Items));
}
[Theory]
[InlineData("Other AS", "Manager")]
[InlineData("Manager", "Other AS")]
public void Genuinely_unlabelled_identity_remains_unresolved_in_both_orders(string first, string second)
{
var result = Parse(("Work Experience", "section_header"), (first, "paragraph"), (second, "paragraph"), ("2022 - 2023", "paragraph"));
Assert.Empty(result.Jobs);
Assert.Equal(new[] { first, second, "2022 - 2023" }, Assert.Single(result.OtherSections).Items);
}
[Fact]
public void Repeated_roles_keep_field_and_bullet_source_occurrences()
{
var result = Parse(("Work Experience", "section_header"), ("Company: A", "paragraph"), ("Title: Engineer", "paragraph"),
("Dates: 2020 - 2021", "paragraph"), ("First role", "list_item"),
("Title: Engineer", "paragraph"), ("Company: A", "paragraph"), ("Dates: 2022 - 2023", "paragraph"), ("Second role", "list_item"));
Assert.Equal(2, result.Jobs.Count);
Assert.Equal("b0002", result.Metadata.Fields["jobs.0.company"].SourceBlockId);
Assert.Equal("b0007", result.Metadata.Fields["jobs.1.company"].SourceBlockId);
Assert.Equal("b0006", result.Metadata.Fields["jobs.1.title"].SourceBlockId);
Assert.Equal("b0009", result.Metadata.Fields["jobs.1.bullets.0"].SourceBlockId);
Assert.Equal(2, result.Metadata.Fields["jobs.1.company"].SourcePage);
}
[Fact]
public void Repeated_section_heading_clears_continuation_state()
{
var result = Parse(("Work Experience", "section_header"), ("Engineer | A | 2020 - 2021", "paragraph"),
("Work Experience", "section_header"), ("Unknown continuation", "list_item"));
Assert.Empty(Assert.Single(result.Jobs).Bullets);
Assert.Equal("Unknown continuation", Assert.Single(Assert.Single(result.OtherSections).Items));
}
[Fact]
public void Explicit_employer_and_job_title_labels_work_without_a_section_heading()
{
var result = Parse(("Name: Alex", "paragraph"), ("Employer: A", "paragraph"), ("Job title: Engineer", "paragraph"), ("2020", "paragraph"));
Assert.Equal("Alex", result.Contact.FullName);
Assert.Equal("A", Assert.Single(result.Jobs).Company);
Assert.Equal("Engineer", result.Jobs[0].Title);
Assert.Equal("2020", result.Jobs[0].Start);
Assert.Null(result.Jobs[0].End);
}
[Fact]
public void Education_preserves_explicit_level_specialism_location_and_missing_dates()
{
var result = Parse(("Education", "section_header"), ("Institution: Example College", "paragraph"),
("Qualification: Diploma", "paragraph"), ("Qualification level: Level 5", "paragraph"),
("Specialism: Computing", "paragraph"), ("Location: Bergen", "paragraph"),
("Qualification: MSc", "paragraph"), ("Institution: Example University", "paragraph"), ("Dates: August 2020 - June 2022", "paragraph"));
Assert.Equal(2, result.Education.Count);
Assert.Equal("Example College", result.Education[0].Institution);
Assert.Equal("Diploma", result.Education[0].Qualification);
Assert.Equal("Level 5", result.Education[0].QualificationLevel);
Assert.Equal(new[] { "Computing" }, result.Education[0].Details);
Assert.Equal("Bergen", result.Education[0].Location);
Assert.Null(result.Education[0].Start);
Assert.Equal("August 2020", result.Education[1].Start);
Assert.Equal("June 2022", result.Education[1].End);
}
[Fact]
public void Header_defined_table_can_reverse_company_title_and_education_columns()
{
var result = Parse(("Work Experience", "section_header"),
("| Company | Title | Dates |\n| --- | --- | --- |\n| A | Engineer | 2020 - 2021 |\n| B | Designer | 2022 - Present |", "table"),
("Education", "section_header"), ("| Institution | Qualification | Dates |\n| --- | --- | --- |\n| College | Diploma | 2010 - 2012 |", "table"));
Assert.Equal(2, result.Jobs.Count);
Assert.Equal("A", result.Jobs[0].Company);
Assert.Equal("Engineer", result.Jobs[0].Title);
Assert.True(result.Jobs[1].IsCurrent);
Assert.Equal("College", Assert.Single(result.Education).Institution);
Assert.Equal("Diploma", result.Education[0].Qualification);
Assert.DoesNotContain(result.Jobs, j => j.Title == "Company");
}
[Fact]
public void Partial_labelled_record_stays_source_and_cannot_supply_dates_to_previous_record()
{
var result = Parse(("Work Experience", "section_header"), ("Engineer | A | 2020 - 2021", "paragraph"),
("Employer: B", "paragraph"), ("2022 - 2023", "paragraph"));
Assert.Equal("2021", Assert.Single(result.Jobs).End);
Assert.Empty(result.Jobs[0].Bullets);
Assert.Equal(new[] { "Employer: B", "2022 - 2023" }, Assert.Single(result.OtherSections).Items);
}
[Fact]
public void Labelled_fields_inside_one_block_keep_literal_multiline_source()
{
var result = Parse(("Arbeidserfaring", "section_header"),
("Arbeidsgiver: Eksempel AS\nStilling: Utvikler\nDatoer: august 2019 - nå\n- Utviklet søk.", "paragraph"));
var job = Assert.Single(result.Jobs);
Assert.Equal("Eksempel AS", job.Company);
Assert.Equal("Utvikler", job.Title);
Assert.Equal("august 2019", job.Start);
Assert.True(job.IsCurrent);
Assert.Equal(new[] { "Utviklet søk." }, job.Bullets);
Assert.Equal("b0002", result.Metadata.Fields["jobs.0.title"].SourceBlockId);
}
[Fact]
public void Unrecognized_table_headers_are_source_rather_than_employment_facts()
{
var result = Parse(("Work Experience", "section_header"),
("| First | Second |\n| --- | --- |\n| A | Engineer |", "table"));
Assert.Empty(result.Jobs);
Assert.Contains(result.OtherSections.SelectMany(s => s.Items), text => text.Contains("A | Engineer"));
}
[Fact]
public void Labelled_project_and_certification_details_are_bounded_to_their_record()
{
var result = Parse(("Projects", "section_header"), ("Project: Atlas", "paragraph"), ("Role: Developer", "paragraph"),
("Location: Oslo", "paragraph"), ("Technologies: C#; SQL", "paragraph"), ("Dates: 2020 - 2021", "paragraph"),
("Description: Built a demo.", "paragraph"), ("Certifications", "section_header"),
("Certificate: Example Certificate", "paragraph"), ("Issuer: Example Institute", "paragraph"),
("Date: 2023", "paragraph"), ("Location: Bergen", "paragraph"), ("Details: Passed practical assessment.", "paragraph"));
Assert.Equal("Atlas", Assert.Single(result.Projects).Name);
Assert.Equal("Developer", result.Projects[0].Role);
Assert.Equal(new[] { "C#", "SQL" }, result.Projects[0].Skills);
Assert.Equal(new[] { "Built a demo." }, result.Projects[0].Bullets);
Assert.Equal("Example Certificate", Assert.Single(result.Certifications).Name);
Assert.Equal("Example Institute", result.Certifications[0].Issuer);
Assert.Equal("2023", result.Certifications[0].Date);
Assert.Equal(new[] { "Passed practical assessment." }, result.Certifications[0].Details);
}
[Theory]
[InlineData("end")]
[InlineData("section")]
[InlineData("pipe")]
public void Legacy_pipe_job_retains_one_paragraph_only_at_a_clear_boundary(string boundary)
{
var blocks = new List<(string Text, string Kind)>
{
("Work Experience", "section_header"), ("Engineer | A | 2020 - 2021", "paragraph"), ("Built tools.", "paragraph")
};
if (boundary == "section") blocks.AddRange(new[] { ("Skills", "section_header"), ("C#", "paragraph") });
if (boundary == "pipe") blocks.Add(("Manager | B | 2022 - Present", "paragraph"));
var result = Parse(blocks.ToArray());
Assert.Equal(new[] { "Built tools." }, result.Jobs[0].Bullets);
Assert.Empty(result.OtherSections);
Assert.Equal("b0003", result.Metadata.Fields["jobs.0.bullets.0"].SourceBlockId);
Assert.Equal("Built tools.", result.Metadata.Fields["jobs.0.bullets.0"].SourceSnippet);
Assert.Equal("legacy-paragraph-continuation", result.Metadata.Fields["jobs.0.bullets.0"].Method);
Assert.Equal("needs_review", result.Metadata.Fields["jobs.0.bullets.0"].ReviewState);
}
[Fact]
public void Legacy_education_pipe_record_retains_a_single_plain_detail()
{
var result = Parse(("Utdanning", "section_header"), ("Bachelor | Universitet | 2014 - 2017", "paragraph"),
("Spesialisering i databaser.", "paragraph"), ("Master | Universitet | 2017 - 2019", "paragraph"), ("Distribuerte systemer.", "paragraph"));
Assert.Equal(2, result.Education.Count);
Assert.Equal(new[] { "Spesialisering i databaser." }, result.Education[0].Details);
Assert.Equal(new[] { "Distribuerte systemer." }, result.Education[1].Details);
Assert.Equal("b0005", result.Metadata.Fields["education.1.details.0"].SourceBlockId);
Assert.Equal("needs_review", result.Metadata.Fields["education.1.details.0"].ReviewState);
}
[Theory]
[InlineData("Atlas | Developer")]
[InlineData("Atlas")]
public void Legacy_project_retains_one_plain_description(string identity)
{
var result = Parse(("Projects", "section_header"), (identity, "paragraph"), ("Built a public demo.", "paragraph"));
Assert.Equal(new[] { "Built a public demo." }, Assert.Single(result.Projects).Bullets);
Assert.Empty(result.OtherSections);
Assert.Equal("b0003", result.Metadata.Fields["projects.0.bullets.0"].SourceBlockId);
}
[Theory]
[InlineData("Other AS", "2022 - Present")]
[InlineData("Other AS", "Company: B")]
[InlineData("First unexplained line", "Second unexplained line")]
public void Legacy_single_paragraph_buffer_abstains_when_more_identity_evidence_arrives(string first, string second)
{
var result = Parse(("Work Experience", "section_header"), ("Engineer | A | 2020 - 2021", "paragraph"),
(first, "paragraph"), (second, "paragraph"));
Assert.Empty(Assert.Single(result.Jobs).Bullets);
Assert.Equal(new[] { first, second }, result.OtherSections.SelectMany(s => s.Items));
}
[Fact]
public void Labelled_multiline_job_does_not_enable_legacy_paragraph_continuation()
{
var result = Parse(("Work Experience", "section_header"), ("Company: A", "paragraph"),
("Title: Engineer", "paragraph"), ("Unlabelled next source", "paragraph"), ("Skills", "section_header"));
Assert.Empty(Assert.Single(result.Jobs).Bullets);
Assert.Equal("Unlabelled next source", Assert.Single(Assert.Single(result.OtherSections).Items));
}
[Fact]
public void Consecutive_unlabelled_certificate_name_issuer_date_stays_unresolved()
{
var result = Parse(("Certifications", "section_header"), ("Demo Certificate", "paragraph"),
("Demo Institute", "paragraph"), ("2021", "paragraph"));
Assert.Empty(result.Certifications);
Assert.Equal(new[] { "Demo Certificate", "Demo Institute", "2021" }, Assert.Single(result.OtherSections).Items);
Assert.Equal("b0003", result.Metadata.Fields["otherSections.0.items.1"].SourceBlockId);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void One_isolated_plain_certificate_name_keeps_existing_contract(bool nextSection)
{
var blocks = new List<(string Text, string Kind)> { ("Certifications", "section_header"), ("Demo Certificate", "paragraph") };
if (nextSection) blocks.Add(("Skills", "section_header"));
var result = Parse(blocks.ToArray());
Assert.Equal("Demo Certificate", Assert.Single(result.Certifications).Name);
Assert.Empty(result.OtherSections);
}
[Fact]
public void Standalone_certificate_list_items_are_separate_explicit_names()
{
var result = Parse(("Certifications", "section_header"), ("First Certificate", "list_item"), ("Second Certificate", "list_item"));
Assert.Equal(new[] { "First Certificate", "Second Certificate" }, result.Certifications.Select(c => c.Name));
Assert.All(result.Certifications, c => { Assert.Null(c.Issuer); Assert.Empty(c.Details); });
Assert.Equal("b0003", result.Metadata.Fields["certifications.1.name"].SourceBlockId);
}
[Fact]
public void Explicit_certification_records_keep_their_own_detail_lists()
{
var result = Parse(("Certifications", "section_header"), ("Certificate: First", "paragraph"),
("Issuer: Institute", "paragraph"), ("Date: 2020", "paragraph"), ("First assessment", "list_item"),
("Second | Other Institute | 2021", "paragraph"), ("Second assessment", "list_item"));
Assert.Equal(2, result.Certifications.Count);
Assert.Equal(new[] { "First assessment" }, result.Certifications[0].Details);
Assert.Equal(new[] { "Second assessment" }, result.Certifications[1].Details);
}
[Fact]
public void Ambiguous_certificate_cluster_cannot_poison_a_following_labelled_record()
{
var result = Parse(("Certifications", "section_header"), ("Demo Certificate", "paragraph"),
("Demo Institute", "paragraph"), ("2021", "paragraph"), ("Certificate: Explicit Certificate", "paragraph"),
("Issuer: Known Institute", "paragraph"), ("Date: 2023", "paragraph"));
Assert.Equal("Explicit Certificate", Assert.Single(result.Certifications).Name);
Assert.Equal("Known Institute", result.Certifications[0].Issuer);
Assert.Equal(new[] { "Demo Certificate", "Demo Institute", "2021" }, Assert.Single(result.OtherSections).Items);
}
[Fact]
public void An_orphan_year_under_certifications_is_not_a_certificate_name()
{
var result = Parse(("Certifications", "section_header"), ("2021", "paragraph"));
Assert.Empty(result.Certifications);
Assert.Equal("2021", Assert.Single(Assert.Single(result.OtherSections).Items));
}
}