feat(cv-qa): map missing records from reviewed source evidence

This commit is contained in:
cesnimda
2026-10-26 19:43:30 +01:00
parent 249d06848a
commit 5e68848cf7
13 changed files with 778 additions and 14 deletions
@@ -72,6 +72,85 @@ public class CvImportReviewedPersistenceTests
return run;
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Source_mapped_new_records_fill_empty_profile_or_roll_back_without_rejected_proposals(bool succeeds)
{
await using var database = await MariaDbDatabase.CreateAsync();
await using var db = database.Context();
var run = await Seed(db);
var blocks = new List<CvImportBlock>();
var sequence = 0;
CvImportRecordMapping Map(string category, params (string Name, string Value)[] values)
{
var fields = values.Select(value =>
{
var block = new CvImportBlock("b" + ++sequence, value.Value, "paragraph", 1);
blocks.Add(block);
return new CvImportMappedField(value.Name, value.Value, block.Id);
}).ToArray();
return new("record-" + sequence, category, null, fields);
}
var mappings = new[]
{
Map("contact", ("fullName", "Åse Synthetic"), ("email", "ase@example.test"), ("phone", "+47 400 00 001"), ("headline", "Engineer"), ("location", "Oslo"), ("website", "https://example.test"), ("linkedIn", "https://linkedin.com/in/synthetic"), ("gitHub", "https://github.com/synthetic"), ("linkLabel", "Portfolio"), ("linkUrl", "https://portfolio.example.test")),
Map("jobs", ("title", "Engineer"), ("company", "First AS"), ("location", "Oslo"), ("start", "2018"), ("end", "2020"), ("bullets", "Built tools."), ("skills", "Rust")),
Map("jobs", ("title", "Lead"), ("company", "Second AS"), ("start", "2021"), ("end", "Present"), ("isCurrent", "true"), ("bullets", "Led delivery.")),
Map("education", ("qualification", "Bachelor"), ("qualificationLevel", "Bachelor"), ("institution", "First University"), ("location", "Bergen"), ("start", "2010"), ("end", "2013"), ("details", "Databases")),
Map("education", ("qualification", "Master"), ("institution", "Second University"), ("start", "2013"), ("end", "2015"), ("details", "Distributed systems")),
Map("certifications", ("name", "Synthetic certificate"), ("issuer", "Example Institute"), ("date", "2022"), ("location", "Online"), ("details", "Recorded assessment")),
Map("projects", ("name", "Atlas"), ("role", "Developer"), ("location", "Remote"), ("start", "2020"), ("end", "2021"), ("bullets", "Built search."), ("skills", "React")),
Map("languages", ("name", "Norsk"), ("level", "Morsmål"), ("notes", "Stated level")), Map("languages", ("name", "English")),
Map("skills", ("value", "Rust"), ("value", "Python")), Map("summary", ("value", "Source-backed professional summary.")),
Map("otherSections", ("title", "Courses"), ("items", "Systems course")), Map("awards", ("value", "Synthetic award")),
Map("publications", ("value", "Synthetic publication")), Map("organisations", ("value", "Synthetic organisation")),
Map("interests", ("value", "Hiking")), Map("references", ("value", "Available on request")),
};
run.RawExtractedText = string.Join('\n', blocks.Select(b => b.Text));
run.DocumentJson = JsonSerializer.Serialize(new CvImportDocument(run.RawExtractedText, false, "application/pdf", 1, run.RawExtractedText.Length, "synthetic.pdf", blocks, Array.Empty<string>(), "synthetic-test-document"));
run.Artifact = new() { OwnerUserId = Owner, OriginalFileName = "synthetic.pdf", StoredFileName = "synthetic.pdf", MimeType = "application/pdf", Sha256 = new string('a', 64) };
await db.SaveChangesAsync();
var artifactId = run.ArtifactId;
var request = new ProfileCvController.ApplyReviewedRequest(0, new[] { new CvImportDecision("skills.0", "keep"), new CvImportDecision("skills.1", "keep") }, mappings);
var result = await Controller(db, succeeds).ApplyReviewed(run.Id, request);
if (succeeds)
{
Assert.IsType<OkObjectResult>(result);
Assert.IsType<OkObjectResult>(await Controller(db).ApplyReviewed(run.Id, request));
}
else Assert.IsType<BadRequestObjectResult>(result);
await db.SaveChangesAsync();
await using var verify = database.Context();
var snapshot = await new CareerProfileService(verify).LoadSnapshotAsync(Owner, default);
Assert.Equal(succeeds ? 1 : 0, snapshot.Revision);
Assert.Equal(succeeds ? 2 : 0, await verify.CareerExperiences.CountAsync());
Assert.Equal(succeeds ? 2 : 0, await verify.CareerEducations.CountAsync());
Assert.Equal(succeeds ? 2 : 0, await verify.CareerLanguages.CountAsync());
Assert.Equal(succeeds ? 1 : 0, await verify.CareerProjects.CountAsync());
Assert.Equal(succeeds ? 1 : 0, await verify.CareerCertifications.CountAsync());
Assert.Equal(succeeds ? new[] { "Python", "Rust" } : Array.Empty<string>(), await verify.CareerSkills.OrderBy(s => s.Name).Select(s => s.Name!).ToArrayAsync());
Assert.Equal(succeeds ? 1 : 0, await verify.CareerProfileVersions.CountAsync());
Assert.Equal(succeeds ? "applied" : "pending_review", (await verify.CvExtractionRuns.SingleAsync()).Status);
if (!succeeds) return;
var profile = snapshot.Profile;
Assert.Equal("Åse Synthetic", profile.Contact.FullName);
Assert.Equal("https://portfolio.example.test", Assert.Single(profile.Contact.Links).Url);
Assert.Equal("2018", profile.Jobs[0].Start);
Assert.Equal("Oslo", profile.Jobs[0].Location);
Assert.True(profile.Jobs[1].IsCurrent);
Assert.Null(profile.Languages.Single(l => l.Name == "English").Level);
Assert.Equal("Databases", Assert.Single(profile.Education[0].Details));
Assert.Equal("React", Assert.Single(profile.Projects[0].Skills));
Assert.Equal("Courses", Assert.Single(profile.OtherSections).Title);
Assert.Single(profile.Awards); Assert.Single(profile.Publications); Assert.Single(profile.Organisations); Assert.Single(profile.Interests); Assert.Single(profile.References); Assert.Single(profile.Summary);
Assert.DoesNotContain("SQL", StructuredCvProfileJson.SerializePersisted(profile));
Assert.Equal(artifactId, profile.Metadata.Fields["jobs.0.title"].SourceArtifactId);
Assert.Equal(run.Id, profile.Metadata.Fields["jobs.0.title"].SourceExtractionRunId);
Assert.Equal("user-source-mapping", profile.Metadata.Fields["jobs.0.title"].Method);
Assert.Contains("Mappings", (await verify.CvExtractionRuns.SingleAsync()).ReviewJson);
}
[Theory]
[InlineData(true)]
[InlineData(false)]