fix(cv): make initial revision atomic
CI and Deploy / test (push) Failing after 8m11s
CI and Deploy / deploy (push) Has been skipped

This commit is contained in:
cesnimda
2026-08-29 02:13:05 +02:00
parent 9cfe622103
commit a3b4941447
4 changed files with 42 additions and 10 deletions
+21
View File
@@ -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;
}
+1
View File
@@ -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.
+13 -10
View File
@@ -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();