feat(cv): redesign document editing with revision-safe letters and writing tools

This commit is contained in:
cesnimda
2026-10-25 22:44:14 +01:00
parent 01e10996c5
commit 2bfaf6c14b
57 changed files with 2859 additions and 118 deletions
+39
View File
@@ -66,6 +66,43 @@ public sealed class AiWorkspaceTests
private static AiGenerateRequest Req(string module, string? mode = null) => new(module, mode, null);
private static async Task SeedLinkedProfileAsync(JobTrackerContext db, int jobId, StructuredCvProfile? profile = null, CvVariantSettings? settings = null, string profileOwner = "user-1")
{
db.CareerProfiles.Add(new CareerProfile { OwnerUserId = profileOwner, ProfileJson = StructuredCvProfileJson.Serialize(profile ?? new StructuredCvProfile { Skills = { "C#" } }) });
db.CvVariants.Add(new CvVariant {
OwnerUserId = "user-1", JobApplicationId = jobId, Name = "Source CV", PublicSlug = Guid.NewGuid().ToString("N"),
SettingsJson = CvVariantSettingsJson.Serialize(settings ?? new()), Version = 1,
CreatedAtUtc = DateTimeOffset.UtcNow, UpdatedAtUtc = DateTimeOffset.UtcNow,
});
await db.SaveChangesAsync();
}
[Theory]
[InlineData("generate", "no-linked-cv")]
[InlineData("improve", "no-linked-cv")]
[InlineData("grammar", "identity-only")]
[InlineData("shorten", "hidden-facts")]
[InlineData("generate", "foreign-profile")]
public async Task Letter_requires_owned_visible_career_facts_before_inference_even_with_draft_and_advert(string action, string source)
{
var (db, svc, ai) = New("user-1");
await using var _ = db;
var jobId = await SeedJobAsync(db, "user-1");
if (source != "no-linked-cv")
{
var profile = new StructuredCvProfile { Contact = new StructuredCvContact { FullName = "Ada Example", Headline = "Engineer" } };
var settings = new CvVariantSettings();
if (source == "hidden-facts") { profile.Skills.Add("C#"); settings.Sections.Add(new() { Key = "skills", Hidden = true }); }
if (source == "foreign-profile") profile.Skills.Add("Foreign profile skill");
await SeedLinkedProfileAsync(db, jobId, profile, settings, source == "foreign-profile" ? "user-2" : "user-1");
}
var error = await Assert.ThrowsAsync<ArgumentException>(() => svc.GenerateAsync("user-1", jobId,
"Unverified legacy fallback claiming 99 years experience", "Ada", new("cover-letter", "professional", "Invent impressive results", "en", "An existing unverified letter draft", action), "p", default));
Assert.Equal("cover_letter_source_required", error.Message);
Assert.Equal(0, ai.Calls);
Assert.Empty(await db.AiInteractions.IgnoreQueryFilters().ToListAsync());
}
[Fact]
public async Task Generate_stores_an_interaction_and_returns_it()
{
@@ -107,6 +144,7 @@ public sealed class AiWorkspaceTests
var (db, svc, _) = New("user-1");
await using var _ = db;
var jobId = await SeedJobAsync(db, "user-1");
await SeedLinkedProfileAsync(db, jobId);
var res = await svc.GenerateAsync("user-1", jobId, "cv", "Ada", Req("cover-letter", "banana"), "p", default);
@@ -209,6 +247,7 @@ public sealed class AiWorkspaceTests
await using var _ = db;
ai.Next = " ";
var jobId = await SeedJobAsync(db, "user-1");
await SeedLinkedProfileAsync(db, jobId);
await Assert.ThrowsAsync<AiUnavailableException>(() => svc.GenerateAsync("user-1", jobId, "cv", "Ada", Req("cover-letter"), "p", default));
}