Improve CV parsing and profile editor flow

This commit is contained in:
2026-03-29 14:29:18 +02:00
parent 99fc94bc18
commit 44000f96f2
18 changed files with 1028 additions and 44 deletions
+156 -1
View File
@@ -280,7 +280,7 @@ public sealed class ProfileCvControllerTests
[Fact]
public async Task Upload_populates_structured_fields_from_flattened_cv_when_ai_json_is_invalid()
{
var rawExtraction = "connor.babbington@cesnimda.co.uk cesnimda.co.uk +47 41 33 44 70 E D U C A T I O N E X T E N D E D D I P L O M A N V Q L E V E L 3 I N I C T 2012 - 2015 F O L L O W A B O U T M E Mid-level system developer with eight years of experience in UK local government, with expertise in full-stack development, backend, frontend and server administration. I N T E R E S T S I am interested in PC and board games, as well as cooking and learning new skills. E X P E R I E N C E S Y S T E M D E V E L O P E R 2015 - 2023 Developed and maintained multiple full-stack applications using C#, Python, Ruby on Rails, SQL, and JavaScript. + Warwickshire County Council, UK C O N T A C T Native English speaker, Norwegian level A2/B1.";
var rawExtraction = "connor.babbington@cesnimda.co.uk cesnimda.co.uk +47 41 33 44 70 E D U C A T I O N E X T E N D E D D I P L O M A N V Q L E V E L 3 I N I C T 2012 - 2015 F O L L O W A B O U T M E Mid-level system developer with eight years of experience in UK local government, with expertise in full-stack development, backend, frontend and server administration. I N T E R E S T S I am interested in PC and board games, as well as cooking and learning new skills. E X P E R I E N C E S Y S T E M D E V E L O P E R 2015 - 2023 Developed and maintained multiple full-stack applications using C#, Python, Ruby on Rails, SQL, and JavaScript. + Warwickshire County Council, UK C O N T A C T Native English speaker, Norwegian level A2/B1, C#, SQL, and public speaking.";
var user = new ApplicationUser { Id = "user-1" };
var userManager = CreateUserManager();
@@ -320,9 +320,164 @@ public sealed class ProfileCvControllerTests
Assert.Contains(structured.Interests, item => item.Contains("board games", StringComparison.OrdinalIgnoreCase) || item.Contains("cooking", StringComparison.OrdinalIgnoreCase));
Assert.Contains(structured.Languages, item => item.Name != null && item.Name.Equals("English", StringComparison.OrdinalIgnoreCase));
Assert.Contains(structured.Languages, item => item.Name != null && item.Name.StartsWith("Norwegian", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(structured.Languages, item => item.Name != null && item.Name.Equals("C#", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(structured.Languages, item => item.Name != null && item.Name.Equals("SQL", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(structured.Languages, item => item.Name != null && item.Name.Contains("public speaking", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(structured.Sections, section => section.Name == "General");
}
[Fact]
public void Structured_cv_normalization_keeps_human_languages_and_drops_skill_noise()
{
var structured = StructuredCvProfileJson.Deserialize("""
{
"version": "1",
"contact": {},
"summary": [],
"jobs": [],
"education": [],
"skills": [],
"languages": [
{ "name": "English", "level": "Native" },
{ "name": "Native Norwegian speaker", "level": null },
{ "name": "French", "level": null },
{ "name": "C#", "level": "Advanced" },
{ "name": "Leadership", "level": null }
],
"interests": [],
"otherSections": []
}
""");
Assert.Collection(
structured.Languages.OrderBy(item => item.Name, StringComparer.OrdinalIgnoreCase),
first =>
{
Assert.Equal("English", first.Name);
Assert.Equal("Native", first.Level);
},
second =>
{
Assert.Equal("Norwegian", second.Name);
Assert.Equal("Native", second.Level);
});
}
[Fact]
public void Structured_cv_normalization_separates_job_title_company_and_tasks()
{
var structured = StructuredCvProfileJson.Deserialize("""
{
"version": "1",
"contact": {},
"summary": [],
"jobs": [
{
"title": "Acme Ltd",
"company": "Senior Backend Developer",
"location": "Oslo",
"start": "2022",
"end": "2024",
"isCurrent": false,
"bullets": [
"Senior Backend Developer",
"Acme Ltd",
"2022 - 2024",
"Built API integrations for recruiter workflows and reduced manual follow-up churn."
],
"skills": [".NET", "SQL"]
},
{
"title": "Lead Engineer at Northwind Council",
"company": null,
"location": "Remote",
"start": "2020",
"end": "Present",
"isCurrent": true,
"bullets": [
"Led platform delivery across case-management and reporting surfaces.",
"Skills: C#, SQL"
],
"skills": ["C#", "SQL"]
}
],
"education": [],
"skills": [],
"languages": [],
"interests": [],
"otherSections": []
}
""");
Assert.Collection(
structured.Jobs,
first =>
{
Assert.Equal("Senior Backend Developer", first.Title);
Assert.Equal("Acme Ltd", first.Company);
Assert.Equal(new[] { "Built API integrations for recruiter workflows and reduced manual follow-up churn." }, first.Bullets);
},
second =>
{
Assert.Equal("Lead Engineer", second.Title);
Assert.Equal("Northwind Council", second.Company);
Assert.Equal(new[] { "Led platform delivery across case-management and reporting surfaces." }, second.Bullets);
});
}
[Fact]
public void Structured_cv_normalization_hardens_contact_links_locations_and_dates()
{
var structured = StructuredCvProfileJson.Deserialize("""
{
"version": "1",
"contact": {
"location": "Tønsberg, Norway",
"website": "https://cesnimda.co.uk/about",
"linkedin": "linkedin.com/in/demo-user?trk=foo"
},
"summary": [],
"jobs": [
{
"title": "System Developer",
"company": "Warwickshire County Council",
"location": "Warwickshire, England, UK",
"start": "Sept 2023",
"end": "1/1/2024",
"isCurrent": false,
"bullets": ["Built APIs"],
"skills": []
},
{
"title": "Developer",
"company": "Demo Co",
"location": "Remote 123",
"start": "Spring 2024",
"end": "Later",
"isCurrent": false,
"bullets": ["Kept services running"],
"skills": []
}
],
"education": [],
"skills": [],
"languages": [],
"interests": [],
"otherSections": []
}
""");
Assert.Equal("Tønsberg, Norway", structured.Contact.Location);
Assert.Equal("cesnimda.co.uk", structured.Contact.Website);
Assert.Equal("https://www.linkedin.com/in/demo-user", structured.Contact.LinkedIn);
Assert.Equal("Warwickshire, England, UK", structured.Jobs[0].Location);
Assert.Equal("Sept 2023", structured.Jobs[0].Start);
Assert.Equal("1/1/2024", structured.Jobs[0].End);
Assert.Null(structured.Jobs[1].Location);
Assert.Null(structured.Jobs[1].Start);
Assert.Null(structured.Jobs[1].End);
}
[Fact]
public async Task Parse_returns_structured_cv_and_persists_it()
{