refactor(db): migrate job workspace notes

Move interview preparation and AI workspace output persistence into an additive provider-aware migration. Preserve reviewed results, uniqueness, and application cascade semantics.
This commit is contained in:
cesnimda
2026-08-30 17:22:04 +02:00
parent 9922426aa5
commit 778b365f9e
9 changed files with 186 additions and 93 deletions
@@ -111,6 +111,8 @@ public sealed class MigrationChainTests
Assert.Contains("CREATE TABLE IF NOT EXISTS `CvUploadArtifacts`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `CvExtractionRuns`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `TailoredCvDrafts`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `InterviewPrepNotes`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `AiWorkspaceNotes`", script, StringComparison.Ordinal);
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
Assert.All(
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
@@ -507,6 +509,76 @@ public sealed class MigrationChainTests
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
[Fact]
public async Task Job_workspace_note_adoption_preserves_outputs_and_cascade_relationships()
{
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
await using var db = Context(connection);
var migrator = db.GetService<IMigrator>();
await migrator.MigrateAsync("20260830126000_AdoptTailoredCvDraftSchema");
await ExecuteAsync(connection, """
INSERT INTO Companies (Name) VALUES ('Fixture company');
INSERT INTO JobApplications
(CompanyId, JobTitle, DateApplied, SavedAt, Status, ResponseReceived, OwnerUserId,
HasResume, HasCoverLetter, HasPortfolio, HasOtherAttachment, IsDeleted)
VALUES
(1, 'Fixture role', '2026-08-01T09:00:00', '2026-07-20T09:00:00',
'Applied', 0, 'owner-fixture', 0, 0, 0, 0, 0);
CREATE TABLE "InterviewPrepNotes" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_InterviewPrepNotes" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL, "JobApplicationId" INTEGER NOT NULL,
"AttachmentContextSignature" TEXT NOT NULL, "Summary" TEXT NOT NULL,
"TalkingPointsJson" TEXT NOT NULL, "LikelyQuestionsJson" TEXT NOT NULL,
"WeakSpotsJson" TEXT NOT NULL, "GeneratedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_InterviewPrepNotes_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
INSERT INTO "InterviewPrepNotes"
("OwnerUserId", "JobApplicationId", "AttachmentContextSignature", "Summary",
"TalkingPointsJson", "LikelyQuestionsJson", "WeakSpotsJson", "GeneratedAtUtc")
VALUES ('owner-fixture', 1, 'attachments-v1', 'Preserve interview summary',
'["point"]', '["question"]', '["gap"]', '2026-08-30T09:00:00+00:00');
CREATE TABLE "AiWorkspaceNotes" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_AiWorkspaceNotes" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL, "JobApplicationId" INTEGER NOT NULL,
"NoteType" TEXT NOT NULL, "AttachmentContextSignature" TEXT NOT NULL,
"ResultJson" TEXT NOT NULL, "GeneratedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_AiWorkspaceNotes_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
INSERT INTO "AiWorkspaceNotes"
("OwnerUserId", "JobApplicationId", "NoteType", "AttachmentContextSignature",
"ResultJson", "GeneratedAtUtc")
VALUES ('owner-fixture', 1, 'candidate-fit', 'attachments-v1',
'{"strength":"preserve me"}', '2026-08-30T09:00:00+00:00');
""");
await migrator.MigrateAsync();
Assert.Equal("Preserve interview summary", await ScalarAsync<string>(connection,
"SELECT Summary FROM InterviewPrepNotes WHERE JobApplicationId = 1;"));
Assert.Equal("{\"strength\":\"preserve me\"}", await ScalarAsync<string>(connection,
"SELECT ResultJson FROM AiWorkspaceNotes WHERE JobApplicationId = 1;"));
await migrator.MigrateAsync("20260830126000_AdoptTailoredCvDraftSchema");
Assert.Equal("attachments-v1", await ScalarAsync<string>(connection,
"SELECT AttachmentContextSignature FROM InterviewPrepNotes WHERE JobApplicationId = 1;"));
await migrator.MigrateAsync();
Assert.Equal(2L, await ScalarAsync<long>(connection, """
SELECT COUNT(*) FROM sqlite_master
WHERE type = 'index' AND name IN (
'IX_InterviewPrepNotes_OwnerUserId_JobApplicationId',
'IX_AiWorkspaceNotes_OwnerUserId_JobApplicationId_NoteType');
"""));
await ExecuteAsync(connection, "DELETE FROM JobApplications WHERE Id = 1;");
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM InterviewPrepNotes;"));
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AiWorkspaceNotes;"));
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
private static JobTrackerContext Context(SqliteConnection connection)
{
var currentUser = new Mock<ICurrentUserService>();