feat(career): add career profile versioning
CI and Deploy / test (push) Failing after 1m54s
CI and Deploy / deploy (push) Has been skipped

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:
cesnimda
2026-07-18 00:58:18 +02:00
parent 2b57d65715
commit f1bf92a4e0
3 changed files with 114 additions and 0 deletions
@@ -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));
}
}