fix(rd21): guard career and job saves with durable revisions

This commit is contained in:
cesnimda
2026-10-23 08:07:23 +02:00
parent 8f2cad67f7
commit 12f342c09c
32 changed files with 1185 additions and 277 deletions
@@ -29,7 +29,7 @@ public sealed class CareerProfileServiceTests
Jobs = { new StructuredCvJob { Title = "Engineer", Company = "Acme" } },
};
var saved = await service.SaveVersionAsync("user-1", profile, "upload", default);
var saved = await service.SaveVersionAsync("user-1", profile, "upload", 0, default);
Assert.False(string.IsNullOrWhiteSpace(saved.Jobs[0].Id));
}
@@ -45,7 +45,7 @@ public sealed class CareerProfileServiceTests
Education = { new StructuredCvEducation { Id = "shared", Qualification = "BSc", Institution = "University" } },
};
var saved = await service.SaveVersionAsync("user-1", profile, "manual", default);
var saved = await service.SaveVersionAsync("user-1", profile, "manual", 0, default);
Assert.Equal("shared", saved.Jobs[0].Id);
Assert.NotEqual(saved.Jobs[0].Id, saved.Education[0].Id);
@@ -80,10 +80,10 @@ public sealed class CareerProfileServiceTests
{
Jobs = { new StructuredCvJob { Title = "Engineer", Company = "Acme" } },
};
await service.SaveVersionAsync("user-1", profile, "upload", default);
await service.SaveVersionAsync("user-1", profile, "upload", 0, default);
var firstId = profile.Jobs[0].Id;
await service.SaveVersionAsync("user-1", profile, "rebuild", default);
await service.SaveVersionAsync("user-1", profile, "rebuild", 1, default);
Assert.Equal(firstId, profile.Jobs[0].Id);
}
@@ -108,8 +108,8 @@ public sealed class CareerProfileServiceTests
var service = new CareerProfileService(db);
var profile = new StructuredCvProfile { Summary = { "First version" } };
await service.SaveVersionAsync("user-1", profile, "upload", default);
await service.SaveVersionAsync("user-1", profile, "improve", default);
await service.SaveVersionAsync("user-1", profile, "upload", 0, default);
await service.SaveVersionAsync("user-1", profile, "improve", 1, default);
var snapshots = await db.CareerProfiles.IgnoreQueryFilters().Where(x => x.OwnerUserId == "user-1").ToListAsync();
var history = await db.CareerProfileVersions.IgnoreQueryFilters().Where(x => x.OwnerUserId == "user-1").ToListAsync();
@@ -130,7 +130,7 @@ public sealed class CareerProfileServiceTests
Jobs = { new StructuredCvJob { Title = "Engineer", Start = "Jan 2020", End = "Present", IsCurrent = true } },
};
var saved = await service.SaveVersionAsync("user-1", profile, "upload", default);
var saved = await service.SaveVersionAsync("user-1", profile, "upload", 0, default);
Assert.Equal("2020-01", saved.Jobs[0].StartDate);
Assert.Null(saved.Jobs[0].EndDate);
@@ -165,7 +165,7 @@ public sealed class CareerProfileServiceTests
await using var db = NewContext("user-1");
var service = new CareerProfileService(db);
await service.SaveVersionAsync("user-1", RichProfile(), "manual", default);
await service.SaveVersionAsync("user-1", RichProfile(), "manual", 0, default);
var loaded = await service.LoadStructuredAsync("user-1", default);
Assert.Equal("Ada Lovelace", loaded.Contact.FullName);
@@ -196,7 +196,7 @@ public sealed class CareerProfileServiceTests
var service = new CareerProfileService(db);
var profile = RichProfile();
await service.SaveVersionAsync("user-1", profile, "manual", default);
await service.SaveVersionAsync("user-1", profile, "manual", 0, default);
var jobKey = profile.Jobs[0].Id;
var loaded = await service.LoadStructuredAsync("user-1", default);
@@ -213,9 +213,9 @@ public sealed class CareerProfileServiceTests
await using var db = NewContext("user-1");
var service = new CareerProfileService(db);
await service.SaveVersionAsync("user-1", RichProfile(), "manual", default); // 2 jobs
await service.SaveVersionAsync("user-1", RichProfile(), "manual", 0, default); // 2 jobs
var trimmed = new StructuredCvProfile { Jobs = { new StructuredCvJob { Title = "Only role", Company = "Solo" } } };
await service.SaveVersionAsync("user-1", trimmed, "manual", default); // 1 job
await service.SaveVersionAsync("user-1", trimmed, "manual", 1, default); // 1 job
var experiences = await db.CareerExperiences.IgnoreQueryFilters().Where(x => x.OwnerUserId == "user-1").ToListAsync();
Assert.Single(experiences);
@@ -225,7 +225,7 @@ public sealed class CareerProfileServiceTests
}
[Fact]
public async Task Load_backfills_relational_children_from_a_pre_phase3_blob()
public async Task Load_reads_a_pre_phase3_blob_without_mutating_canonical_children()
{
await using var db = NewContext("user-1");
// Simulate a profile that predates Phase 3: a CareerProfile with a ProfileJson blob but no
@@ -239,8 +239,8 @@ public sealed class CareerProfileServiceTests
var loaded = await service.LoadStructuredAsync("user-1", default);
Assert.Equal(2, loaded.Jobs.Count);
// The backfill materialized the relational rows.
Assert.Equal(2, await db.CareerExperiences.IgnoreQueryFilters().CountAsync(x => x.OwnerUserId == "user-1"));
// Reads cannot materialize unversioned children alongside a concurrent replacement.
Assert.Equal(0, await db.CareerExperiences.IgnoreQueryFilters().CountAsync(x => x.OwnerUserId == "user-1"));
}
[Fact]
@@ -260,8 +260,8 @@ public sealed class CareerProfileServiceTests
{
await using var db = NewContext("user-1");
var service = new CareerProfileService(db);
await service.SaveVersionAsync("user-1", new StructuredCvProfile { Summary = { "v1" } }, "upload", default);
await service.SaveVersionAsync("user-1", new StructuredCvProfile { Summary = { "v2" } }, "improve", default);
await service.SaveVersionAsync("user-1", new StructuredCvProfile { Summary = { "v1" } }, "upload", 0, default);
await service.SaveVersionAsync("user-1", new StructuredCvProfile { Summary = { "v2" } }, "improve", 1, default);
var versions = await service.ListVersionsAsync("user-1", default);
@@ -276,10 +276,10 @@ public sealed class CareerProfileServiceTests
{
await using var db = NewContext("user-1");
var service = new CareerProfileService(db);
await service.SaveVersionAsync("user-1", new StructuredCvProfile { Summary = { "original" } }, "upload", default); // v1
await service.SaveVersionAsync("user-1", new StructuredCvProfile { Summary = { "edited" } }, "manual", default); // v2
await service.SaveVersionAsync("user-1", new StructuredCvProfile { Summary = { "original" } }, "upload", 0, default); // v1
await service.SaveVersionAsync("user-1", new StructuredCvProfile { Summary = { "edited" } }, "manual", 1, default); // v2
var restored = await service.RestoreVersionAsync("user-1", 1, default);
var restored = await service.RestoreVersionAsync("user-1", 1, 2, default);
Assert.NotNull(restored);
Assert.Equal(new[] { "original" }, restored!.Summary);
@@ -296,8 +296,8 @@ public sealed class CareerProfileServiceTests
{
await using var db = NewContext("user-1");
var service = new CareerProfileService(db);
await service.SaveVersionAsync("user-1", new StructuredCvProfile(), "upload", default);
await service.SaveVersionAsync("user-1", new StructuredCvProfile(), "upload", 0, default);
Assert.Null(await service.RestoreVersionAsync("user-1", 99, default));
Assert.Null(await service.RestoreVersionAsync("user-1", 99, 1, default));
}
}