feat(career): add career profile versioning
Phase 3, version history (list + restore). CareerProfileVersions was already
populated on every save; this makes it usable.
- ICareerProfileService.ListVersionsAsync — the append-only history, newest first,
with the current version flagged.
- RestoreVersionAsync — reapplies a past snapshot NON-DESTRUCTIVELY: it is re-saved
as a new version, so the current state stays in history and the restore is itself
reversible. Syncs the relational children + blob projection like any save.
- Endpoints: GET /career/profile/versions, POST /career/profile/versions/{v}/restore.
Tests (+4): versions listed newest-first with current flagged; restore reapplies
an old snapshot as a new version (history preserved, reversible); restore of a
missing version returns null.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -209,4 +209,50 @@ public sealed class CareerProfileServiceTests
|
||||
Assert.Empty(loaded.Jobs);
|
||||
Assert.Empty(loaded.Skills);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Versions_are_listed_newest_first_with_the_current_flagged()
|
||||
{
|
||||
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);
|
||||
|
||||
var versions = await service.ListVersionsAsync("user-1", default);
|
||||
|
||||
Assert.Equal(new[] { 2, 1 }, versions.Select(v => v.Version));
|
||||
Assert.True(versions[0].IsCurrent);
|
||||
Assert.False(versions[1].IsCurrent);
|
||||
Assert.Equal("improve", versions[0].Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Restore_reapplies_an_old_snapshot_non_destructively()
|
||||
{
|
||||
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
|
||||
|
||||
var restored = await service.RestoreVersionAsync("user-1", 1, default);
|
||||
|
||||
Assert.NotNull(restored);
|
||||
Assert.Equal(new[] { "original" }, restored!.Summary);
|
||||
// Restore is a new version (v3), so history is preserved and the restore is reversible.
|
||||
var versions = await service.ListVersionsAsync("user-1", default);
|
||||
Assert.Equal(new[] { 3, 2, 1 }, versions.Select(v => v.Version));
|
||||
Assert.Equal("restore:v1", versions[0].Source);
|
||||
var loaded = await service.LoadStructuredAsync("user-1", default);
|
||||
Assert.Equal(new[] { "original" }, loaded.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Restore_of_a_missing_version_returns_null()
|
||||
{
|
||||
await using var db = NewContext("user-1");
|
||||
var service = new CareerProfileService(db);
|
||||
await service.SaveVersionAsync("user-1", new StructuredCvProfile(), "upload", default);
|
||||
|
||||
Assert.Null(await service.RestoreVersionAsync("user-1", 99, default));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user