diff --git a/JobTrackerApi.Tests/CvBuilderTests.cs b/JobTrackerApi.Tests/CvBuilderTests.cs index f376abc..0359a32 100644 --- a/JobTrackerApi.Tests/CvBuilderTests.cs +++ b/JobTrackerApi.Tests/CvBuilderTests.cs @@ -1,6 +1,7 @@ using JobTrackerApi.Data; using JobTrackerApi.Models; using JobTrackerApi.Services; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Moq; using Xunit; @@ -366,6 +367,26 @@ public sealed class CvBuilderTests Assert.True(versions[0].IsCurrent); } + [Fact] + public async Task Create_persists_variant_and_initial_history_on_a_relational_store() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + var options = new DbContextOptionsBuilder().UseSqlite(connection).Options; + var currentUser = new Mock(); + currentUser.SetupGet(service => service.UserId).Returns("user-1"); + await using var db = new JobTrackerContext(options, currentUser.Object); + await db.Database.EnsureCreatedAsync(); + var service = new CvVariantService(db, new CareerProfileService(db), new ThemedCvRenderer()); + + var created = await service.CreateAsync("user-1", "Atomic CV", null, new CvVariantSettings(), default); + + Assert.Single(await db.CvVariants.IgnoreQueryFilters().Where(item => item.Id == created.Id).ToListAsync()); + var history = await db.CvVariantVersions.IgnoreQueryFilters().Where(item => item.CvVariantId == created.Id).ToListAsync(); + Assert.Single(history); + Assert.Equal(1, history[0].Version); + } + [Fact] public async Task Restore_reapplies_old_settings_as_a_new_version() { diff --git a/JobTrackerApi/Services/CvVariantService.cs b/JobTrackerApi/Services/CvVariantService.cs index 4873c6b..918ebcd 100644 --- a/JobTrackerApi/Services/CvVariantService.cs +++ b/JobTrackerApi/Services/CvVariantService.cs @@ -81,6 +81,12 @@ public sealed class CvVariantService : ICvVariantService { if (jobApplicationId is not null && !await CanAssociateJobAsync(ownerUserId, jobApplicationId.Value, ct)) throw new ArgumentException("The job application is unavailable.", nameof(jobApplicationId)); + // The first save is required to obtain the generated variant id used by its initial + // history row. Keep both saves in one relational transaction so a concurrent autosave + // cannot slip between them and leave a created CV without version 1. + await using var transaction = _db.Database.IsRelational() + ? await _db.Database.BeginTransactionAsync(ct) + : null; var now = DateTimeOffset.UtcNow; var normalized = CvVariantSettingsJson.Normalize(settings); var variant = new CvVariant @@ -98,6 +104,7 @@ public sealed class CvVariantService : ICvVariantService await _db.SaveChangesAsync(ct); AppendVersion(variant, "create"); await _db.SaveChangesAsync(ct); + if (transaction is not null) await transaction.CommitAsync(ct); return variant; } diff --git a/docs/todo/work.md b/docs/todo/work.md index c069a8b..4748141 100644 --- a/docs/todo/work.md +++ b/docs/todo/work.md @@ -20,6 +20,7 @@ - [x] Diagnosed release 273's assertion-free Node exit 139 and added a single full-suite retry only for that runner crash signature; ordinary frontend test failures remain blocking. - [x] Fixed fresh SQLite/MySQL startup ordering so the migration-owned UI-language column is not pre-created by schema reconciliation, then proved the full fresh-database path in Playwright. - [x] Visually verified the revised builder at 375 px, 768 px, and 1440 px in light/dark mode, including multiple expanded sections, add-content focus return, Customize/AI surfaces, responsive overflow, multi-page preview and searchable PDF export. +- [x] Updated the full browser suite for the consolidated Workspace and revised Builder controls, then fixed CV creation to atomically persist the document and initial history revision under concurrent autosave load; all 10 Playwright flows pass. - [x] Rechecked branch/status, recent CV work, existing tests/build tooling, and current builder architecture. - [x] Traced builder persistence, shared preview/PDF rendering, AI endpoint, extraction pipeline, confidence/diff review, and reference CV assets. - [x] Rendered and inspected the supplied two-page reference PDF and reviewed its HTML typography/theme tokens without modifying the originals. diff --git a/job-tracker-ui/e2e/smoke.spec.ts b/job-tracker-ui/e2e/smoke.spec.ts index cdf7167..d815683 100644 --- a/job-tracker-ui/e2e/smoke.spec.ts +++ b/job-tracker-ui/e2e/smoke.spec.ts @@ -236,11 +236,11 @@ test("the dedicated application workspace survives deep links, long data and uns await page.goForward(); await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}$`)); - const workspaceNav = page.getByRole("navigation", { name: "Workspace sections" }); - await workspaceNav.getByRole("button", { name: "Cover Letter", exact: true }).click(); + const workspaceNav = page.getByRole("tablist", { name: "Workspace sections" }); + await workspaceNav.getByRole("tab", { name: "Cover Letter", exact: true }).click(); await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=cover-letter$`)); await page.getByLabel("Application answer").fill("A reviewed answer that must not be lost."); - await workspaceNav.getByRole("button", { name: "Match", exact: true }).click(); + await workspaceNav.getByRole("tab", { name: "Analysis", exact: true }).click(); await expect(page.getByRole("dialog", { name: "Unsaved application changes" })).toBeVisible(); await page.getByRole("button", { name: "Keep editing" }).click(); await expect(page.getByLabel("Application answer")).toHaveValue("A reviewed answer that must not be lost."); @@ -268,8 +268,9 @@ test("the dedicated application workspace survives deep links, long data and uns expect(overflow).toBeLessThanOrEqual(1); } - await workspaceNav.getByRole("button", { name: "Job Details", exact: true }).click(); - await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=job-details$`)); + await workspaceNav.getByRole("tab", { name: "Overview", exact: true }).click(); + await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=overview$`)); + await page.getByRole("button", { name: "Job details", exact: true }).click(); await expect(page.getByText("Ask about platform ownership and the incident response rotation.")).toBeVisible(); await expect(page.getByText("<<>>")).toHaveCount(0); @@ -315,25 +316,27 @@ test("Career Workspace loads from the authenticated application shell", async ({ await expect(page).toHaveURL(/\/career\/builder\/\d+$/); await expect(page.getByLabel("CV name")).toBeVisible(); + await page.getByRole("tab", { name: "Content" }).click(); await page.getByRole("button", { name: "Expand Professional Summary" }).click(); await page.getByRole("button", { name: "Expand Experience" }).click(); await expect(page.getByRole("button", { name: "Collapse Professional Summary" })).toHaveAttribute("aria-expanded", "true"); await expect(page.getByRole("button", { name: "Collapse Experience" })).toHaveAttribute("aria-expanded", "true"); - await page.getByLabel("Section type").click(); - await page.getByRole("option", { name: "Additional Experience" }).click(); - await page.getByRole("button", { name: "Add section" }).click(); + await page.getByRole("button", { name: "Add content" }).click(); + const addContentDialog = page.getByRole("dialog", { name: "Add content" }); + const additionalExperienceCard = addContentDialog.getByRole("heading", { name: "Additional Experience" }).locator(".."); + await additionalExperienceCard.getByRole("button", { name: "Add", exact: true }).click(); await page.getByRole("button", { name: "Expand Additional Experience" }).click(); await page.getByRole("button", { name: "Add entry" }).click(); await page.getByRole("textbox", { name: "Entry 1", exact: true }).fill("Maintained a community software project."); await page.getByRole("tab", { name: "Template" }).click(); await page.getByRole("button").filter({ hasText: /^CodeTechnical/ }).click(); - await page.getByRole("tab", { name: "Design" }).click(); + await page.getByRole("tab", { name: "Customize" }).click(); await page.getByLabel("Skills presentation").click(); await page.getByRole("option", { name: "Bullet list" }).click(); await page.locator('input[type="color"]').fill("#126b55"); - await expect(page.getByRole("tab", { name: "AI" })).toHaveCount(0); + await expect(page.getByRole("tab", { name: "AI Tools" })).toBeVisible(); await page.evaluate(() => window.localStorage.setItem("jobtracker.themeMode", "dark")); await page.reload();