Files
jobtrackingapp/JobTrackerApi.Tests/CvExtractionCoverageTests.cs
T
cesnimda 56fed05d70
CI and Deploy / test (push) Successful in 2m42s
CI and Deploy / deploy (push) Failing after 9s
feat: complete phase 2 UX improvements
2026-07-30 21:35:52 +02:00

147 lines
5.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Reflection;
using JobTrackerApi.Controllers;
using JobTrackerApi.Models;
using Xunit;
namespace JobTrackerApi.Tests;
// Phase 2.1-b — CV extraction coverage. The structured model and StructuredCvProfileJson.FromSections
// already map Projects, Certifications and Languages headings into the profile; the gap was upstream
// (the AI /cv/normalize prompt never emitted those headings, so the sections were dropped). These
// tests lock the C# side so that once the normalized markdown carries # Projects / # Certifications /
// # Languages, they reach the structured profile — and so a future change can't silently regress it.
//
// Content shapes mirror what the (fixed) normalizer produces for the benchmark CV
// (Connor Babbington): a Projects section, and languages stated as "Name: Level".
public sealed class CvExtractionCoverageTests
{
private static StructuredCvSection Section(string name, string content) =>
new() { Name = name, Content = content };
[Fact]
public void FromSections_maps_a_Projects_heading_into_structured_projects()
{
var profile = StructuredCvProfileJson.FromSections(new[]
{
Section("Projects",
"JobTrack\nFull-stack job-application tracker (React, ASP.NET Core, SQLite, Docker).\n\n" +
"InboxIntel\nGmail analytics and safe bulk-cleanup tool in .NET 8 with PostgreSQL."),
});
Assert.Equal(2, profile.Projects.Count);
Assert.Contains(profile.Projects, p => p.Name == "JobTrack");
Assert.Contains(profile.Projects, p => p.Name == "InboxIntel");
}
[Fact]
public void FromSections_maps_a_Certifications_heading_into_structured_certifications()
{
var profile = StructuredCvProfileJson.FromSections(new[]
{
Section("Certifications",
"Extended Diploma NVQ Level 3 in ICT\n\nAZ-900 Azure Fundamentals"),
});
Assert.NotEmpty(profile.Certifications);
Assert.Contains(profile.Certifications, c => (c.Name ?? "").Contains("NVQ", System.StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void FromSections_maps_a_Languages_heading_with_levels()
{
var profile = StructuredCvProfileJson.FromSections(new[]
{
Section("Languages", "English: Native\nNorwegian: B1"),
});
Assert.Equal(2, profile.Languages.Count);
Assert.Contains(profile.Languages, l => l.Name == "English" && (l.Level ?? "").Contains("Native"));
Assert.Contains(profile.Languages, l => l.Name == "Norwegian" && (l.Level ?? "").Contains("B1"));
}
// The benchmark CV has all four rich sections; confirm they coexist without one clobbering another.
[Fact]
public void FromSections_populates_projects_certifications_and_languages_together()
{
var profile = StructuredCvProfileJson.FromSections(new[]
{
Section("Skills", "C#\n.NET\nDocker"),
Section("Projects", "JobTrack\nJob-application tracker."),
Section("Certifications", "NVQ Level 3 in ICT"),
Section("Languages", "English: Native\nNorwegian: B1"),
});
Assert.NotEmpty(profile.Skills);
Assert.Single(profile.Projects);
Assert.NotEmpty(profile.Certifications);
Assert.Equal(2, profile.Languages.Count);
}
[Fact]
public void Normalized_markdown_strips_skill_group_labels()
{
var profile = InvokeProfileBuilder("""
# Skills
Development: C#
DevOps & Infrastructure: Docker
Practices: CI/CD
""");
Assert.Equal(new[] { "C#", "CI/CD", "Docker" }, profile.Skills);
Assert.DoesNotContain(profile.Skills, skill => skill.StartsWith("Development", System.StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Normalized_markdown_separates_glued_dates_from_job_title()
{
var profile = InvokeProfileBuilder("""
# Work Experience
20152023System Developer
Warwickshire County Council, UK
- Built APIs
""");
var job = Assert.Single(profile.Jobs);
Assert.Equal("2015", job.Start);
Assert.Equal("2023", job.End);
Assert.Contains("System Developer", job.Title ?? string.Empty);
}
[Fact]
public void Extraction_repairs_common_utf8_as_latin1_mojibake()
{
var method = typeof(ProfileCvController).GetMethod("RepairKnownMojibake", BindingFlags.NonPublic | BindingFlags.Static)!;
var repaired = Assert.IsType<string>(method.Invoke(null, new object[] { "Tønsberg 2015–2023" }));
Assert.Equal("Tønsberg 20152023", repaired);
}
[Fact]
public void Earlier_part_time_roles_become_separate_experience_entries()
{
var profile = InvokeProfileBuilder("""
# Work Experience
System Developer
Warwickshire County Council
20152023
- Built APIs
Earlier roles (part-time)
- Sales Assistant Royal Vapes | 20132015
- Labourer The Hodcarrier | 20142016
- Lifeguard Nuffield Health | 20122014
""");
Assert.Contains(profile.Jobs, job => job.Title == "Sales Assistant" && job.Company == "Royal Vapes");
Assert.Contains(profile.Jobs, job => job.Title == "Labourer" && job.Company == "The Hodcarrier");
Assert.Contains(profile.Jobs, job => job.Title == "Lifeguard" && job.Company == "Nuffield Health");
Assert.Equal(4, profile.Jobs.Count);
}
private static StructuredCvProfile InvokeProfileBuilder(string markdown)
{
var method = typeof(ProfileCvController).GetMethod("BuildStructuredCvFromNormalizedMarkdown", BindingFlags.NonPublic | BindingFlags.Static)!;
return Assert.IsType<StructuredCvProfile>(method.Invoke(null, new object[] { markdown }));
}
}