using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; namespace ClaudeDo.Data.Tests; /// /// AddTaskDependencies moves every tasks.depends_on_task_id value into the new /// task_dependencies table and then drops the column. Runs a real Migrate() — the /// EnsureCreated fixtures every other Data.Tests file uses skip migrations entirely and would /// never notice a data copy that silently dropped rows (see /// .claudedo/traps/ef-migration-parallel-designer-collision.md). /// public sealed class TaskDependencyMigrationTests : IDisposable { private const string PreviousMigrationId = "20260902102003_AddExpectsNoDiff"; private readonly string _dbPath; private readonly DbContextOptions _options; public TaskDependencyMigrationTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_deps_{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 Existing_depends_on_links_survive_as_one_element_sets() { SeedLegacyDatabase(); using (var ctx = new ClaudeDoDbContext(_options)) ctx.Database.Migrate(); using var check = new ClaudeDoDbContext(_options); Assert.Empty(check.Database.GetPendingMigrations()); var edges = check.TaskDependencies.AsNoTracking() .OrderBy(d => d.TaskId) .Select(d => new { d.TaskId, d.DependsOnTaskId }) .ToList(); Assert.Equal(2, edges.Count); Assert.Equal(("t2", "t1"), (edges[0].TaskId, edges[0].DependsOnTaskId)); Assert.Equal(("t3", "t2"), (edges[1].TaskId, edges[1].DependsOnTaskId)); // The tasks themselves are untouched, and the independent one gained no edge. Assert.Equal(4, check.Tasks.Count()); Assert.DoesNotContain("t4", edges.Select(e => e.TaskId)); } [Fact] public void Column_is_gone_after_the_migration() { SeedLegacyDatabase(); using var ctx = new ClaudeDoDbContext(_options); ctx.Database.Migrate(); var conn = ctx.Database.GetDbConnection(); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM pragma_table_info('tasks') WHERE name = 'depends_on_task_id'"; Assert.Equal(0L, Convert.ToInt64(cmd.ExecuteScalar())); conn.Close(); } // Migrates *to* the migration before this one, then seeds via raw SQL: EnsureCreated would // build today's schema (no depends_on_task_id column at all), so there'd be nothing to copy. private void SeedLegacyDatabase() { using var seed = new ClaudeDoDbContext(_options); seed.Database.GetService().Migrate(PreviousMigrationId); var conn = seed.Database.GetDbConnection(); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = """ INSERT INTO lists (id, name, created_at) VALUES ('l1', 'List', '2026-01-01 00:00:00'); INSERT INTO tasks (id, number, list_id, title, status, created_at) VALUES ('t1', 1, 'l1', 'first', 'done', '2026-01-01 00:00:00'); INSERT INTO tasks (id, number, list_id, title, status, created_at, depends_on_task_id) VALUES ('t2', 2, 'l1', 'second', 'queued', '2026-01-01 00:00:00', 't1'); INSERT INTO tasks (id, number, list_id, title, status, created_at, depends_on_task_id) VALUES ('t3', 3, 'l1', 'third', 'queued', '2026-01-01 00:00:00', 't2'); INSERT INTO tasks (id, number, list_id, title, status, created_at) VALUES ('t4', 4, 'l1', 'independent', 'idle', '2026-01-01 00:00:00'); """; cmd.ExecuteNonQuery(); conn.Close(); } }