using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Worker.Lifecycle; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.Extensions.Logging.Abstractions; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Lifecycle; public sealed class AttachmentOrphanRecoveryTests : IDisposable { private readonly DbFixture _db = new(); private readonly string _attachRoot; public AttachmentOrphanRecoveryTests() { _attachRoot = Path.Combine(Path.GetTempPath(), $"claudedo_orph_{Guid.NewGuid():N}"); Directory.CreateDirectory(_attachRoot); } public void Dispose() { _db.Dispose(); try { Directory.Delete(_attachRoot, recursive: true); } catch { } } [Fact] public async Task StartAsync_DeletesOrphanDir_KeepsLiveTaskDir() { // Seed one real task. string listId = Guid.NewGuid().ToString(); string liveTaskId = Guid.NewGuid().ToString(); using (var ctx = _db.CreateContext()) { ctx.Lists.Add(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow }); ctx.Tasks.Add(new TaskEntity { Id = liveTaskId, ListId = listId, Title = "T", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, }); await ctx.SaveChangesAsync(); } // Create two attachment directories: one for the live task, one orphan. var store = new AttachmentStore(_attachRoot); var liveDir = store.TaskDir(liveTaskId); var orphanId = Guid.NewGuid().ToString(); var orphanDir = store.TaskDir(orphanId); Directory.CreateDirectory(liveDir); Directory.CreateDirectory(orphanDir); var sut = new AttachmentOrphanRecovery( _db.CreateFactory(), store, NullLogger.Instance); await sut.StartAsync(CancellationToken.None); Assert.True(Directory.Exists(liveDir), "Live task dir should be kept"); Assert.False(Directory.Exists(orphanDir), "Orphan dir should be deleted"); } [Fact] public async Task StartAsync_NoAttachmentRoot_IsNoop() { // Use a root that does not exist — should complete without throwing. var missingRoot = Path.Combine(Path.GetTempPath(), $"claudedo_missing_{Guid.NewGuid():N}"); var store = new AttachmentStore(missingRoot); var sut = new AttachmentOrphanRecovery( _db.CreateFactory(), store, NullLogger.Instance); await sut.StartAsync(CancellationToken.None); // must not throw } }