using ClaudeDo.Data.Models; using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Data.Tests; // The 40 incremental migrations were squashed into one InitialCreate, so every existing database // carries a history EF no longer recognises. MigrateAndConfigure re-stamps those instead of // re-creating tables — this is the only test covering that path (every other Data.Tests fixture // uses EnsureCreated, which skips migrations entirely). // Replaces TaskNumberMigrationTests, whose subject (the AddTaskNumbers backfill) was squashed away. public sealed class MigrationBaselineTests : IDisposable { private const string LastPreSquashMigrationId = "20260825063230_AddPrimeActionKind"; private const string SquashedMigrationId = "20260826094154_InitialCreate"; private readonly string _dbPath; private readonly DbContextOptions _options; public MigrationBaselineTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_baseline_{Guid.NewGuid():N}.db"); _options = new DbContextOptionsBuilder() .UseSqlite($"Data Source={_dbPath}") .Options; } public void Dispose() { Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); foreach (var suffix in new[] { "", "-wal", "-shm" }) try { File.Delete(_dbPath + suffix); } catch { } } [Fact] public void Fresh_database_migrates_and_seeds() { using var ctx = new ClaudeDoDbContext(_options); ClaudeDoDbContext.MigrateAndConfigure(ctx); Assert.Equal(new[] { SquashedMigrationId }, ctx.Database.GetAppliedMigrations()); Assert.NotEmpty(ctx.Lists); } [Fact] public void Fully_migrated_pre_squash_database_is_restamped_and_keeps_its_rows() { SeedCurrentSchemaWithHistory(LastPreSquashMigrationId, taskTitle: "survivor"); using var ctx = new ClaudeDoDbContext(_options); ClaudeDoDbContext.MigrateAndConfigure(ctx); Assert.Equal(new[] { SquashedMigrationId }, ctx.Database.GetAppliedMigrations()); Assert.Equal("survivor", ctx.Tasks.Single().Title); } [Fact] public void Restamping_is_idempotent() { SeedCurrentSchemaWithHistory(LastPreSquashMigrationId, taskTitle: "survivor"); using (var first = new ClaudeDoDbContext(_options)) ClaudeDoDbContext.MigrateAndConfigure(first); using var second = new ClaudeDoDbContext(_options); ClaudeDoDbContext.MigrateAndConfigure(second); Assert.Equal(new[] { SquashedMigrationId }, second.Database.GetAppliedMigrations()); Assert.Equal("survivor", second.Tasks.Single().Title); } [Fact] public void Database_stuck_mid_chain_fails_loudly_instead_of_being_stamped() { SeedCurrentSchemaWithHistory("20260810115437_AddFailureReason", taskTitle: "stale"); using var ctx = new ClaudeDoDbContext(_options); var ex = Assert.Throws(() => ClaudeDoDbContext.MigrateAndConfigure(ctx)); Assert.Contains("Reinstall ClaudeDo", ex.Message); } [Fact] public void Pre_ef_database_without_history_is_baselined() { SeedCurrentSchemaWithHistory(historyId: null, taskTitle: "legacy"); using var ctx = new ClaudeDoDbContext(_options); ClaudeDoDbContext.MigrateAndConfigure(ctx); Assert.Equal(new[] { SquashedMigrationId }, ctx.Database.GetAppliedMigrations()); Assert.Equal("legacy", ctx.Tasks.Single().Title); } // EnsureCreated builds today's schema without touching __EFMigrationsHistory, which is exactly // the shape of a database that was fully migrated by the old chain: current tables, old history. private void SeedCurrentSchemaWithHistory(string? historyId, string taskTitle) { using var seed = new ClaudeDoDbContext(_options); seed.Database.EnsureCreated(); var list = new ListEntity { Id = "l1", Name = "List", CreatedAt = DateTime.UtcNow }; seed.Lists.Add(list); seed.Tasks.Add(new TaskEntity { Id = "t1", ListId = list.Id, Title = taskTitle, Number = 1, CreatedAt = DateTime.UtcNow, }); seed.SaveChanges(); if (historyId is null) return; var conn = seed.Database.GetDbConnection(); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = $""" CREATE TABLE "__EFMigrationsHistory" ( "MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY, "ProductVersion" TEXT NOT NULL ); INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") VALUES ('{historyId}', '8.0.11'); """; cmd.ExecuteNonQuery(); conn.Close(); } }