refactor(db): migrate AI interaction history

Move append-only AI results into an additive provider-aware migration. Preserve generated content, usage counters, indexes, and application cascade semantics.
This commit is contained in:
cesnimda
2026-08-30 18:48:42 +02:00
parent bc28893b6f
commit ae57d1afde
9 changed files with 148 additions and 54 deletions
@@ -656,6 +656,53 @@ public sealed class MigrationChainTests
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
[Fact]
public async Task Ai_interaction_adoption_preserves_history_usage_and_cascade()
{
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("20260830128000_AdoptCvVariantSchema");
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);
INSERT INTO AiInteractions
(OwnerUserId, JobApplicationId, Module, Mode, Title, Provider, ResultJson,
InputCharacterCount, OutputCharacterCount, EstimatedTokenCount, CreatedAtUtc)
VALUES
('owner-fixture', 1, 'cover-letter', 'professional', 'Cover letter draft',
'fixture-provider', '{"text":"preserve me"}', 1200, 640, 460,
'2026-08-30T09:00:00+00:00');
""");
await migrator.MigrateAsync();
Assert.Equal("{\"text\":\"preserve me\"}", await ScalarAsync<string>(connection,
"SELECT ResultJson FROM AiInteractions WHERE Id = 1;"));
Assert.Equal(460L, await ScalarAsync<long>(connection,
"SELECT EstimatedTokenCount FROM AiInteractions WHERE Id = 1;"));
await migrator.MigrateAsync("20260830128000_AdoptCvVariantSchema");
Assert.Equal("professional", await ScalarAsync<string>(connection,
"SELECT Mode FROM AiInteractions WHERE Id = 1;"));
await migrator.MigrateAsync();
Assert.Equal(2L, await ScalarAsync<long>(connection, """
SELECT COUNT(*) FROM sqlite_master
WHERE type = 'index' AND name IN (
'IX_AiInteractions_JobApplicationId',
'IX_AiInteractions_Owner_Job_Module_Created');
"""));
await ExecuteAsync(connection, "DELETE FROM JobApplications WHERE Id = 1;");
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AiInteractions;"));
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
private static JobTrackerContext Context(SqliteConnection connection)
{
var currentUser = new Mock<ICurrentUserService>();