fix(cv): make initial revision atomic
This commit is contained in:
@@ -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<JobTrackerContext>().UseSqlite(connection).Options;
|
||||
var currentUser = new Mock<ICurrentUserService>();
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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("<<<APPLICATION_ANSWER_DRAFT>>>")).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();
|
||||
|
||||
Reference in New Issue
Block a user