using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Tests.Infrastructure; using Xunit; namespace ClaudeDo.Worker.Tests.Hub; /// UpdateListConfig's "all fields blank -> delete the row" branch used to delete unconditionally, /// silently dropping fields the caller doesn't own. It may only delete when nothing else is stored; /// the preserve-the-stored-value side is covered by ListConfigTicketProjectTests. public sealed class ListConfigHubTests : IDisposable { private readonly DbFixture _db = new(); public void Dispose() => _db.Dispose(); private WorkerHub CreateHub() { var factory = _db.CreateFactory(); var broadcaster = new HubBroadcaster(new CapturingHubContext()); var hub = new WorkerHub( null!, null!, null!, null!, broadcaster, factory, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")), new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!); hub.Clients = new FakeHubCallerClients(new RecordingClientProxy()); hub.Context = new FakeHubCallerContext(); return hub; } // Each helper opens (and disposes) its own short-lived context/repository -- ListRepository's // GetConfigAsync doesn't AsNoTracking(), so reusing one long-lived instance across a hub call // that writes via a *different* context would return a stale, identity-mapped entity. private async Task SeedListAsync() { var listId = Guid.NewGuid().ToString(); await using var ctx = _db.CreateContext(); await new ListRepository(ctx).AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow }); return listId; } private async Task SeedConfigAsync(string listId, string? model = null) { await using var ctx = _db.CreateContext(); await new ListRepository(ctx).SetConfigAsync(new ListConfigEntity { ListId = listId, Model = model }); } private async Task GetConfigAsync(string listId) { await using var ctx = _db.CreateContext(); return await new ListRepository(ctx).GetConfigAsync(listId); } [Fact] public async Task UpdateListConfig_AllBlank_NoUnrelatedSettings_DeletesRow() { var hub = CreateHub(); var listId = await SeedListAsync(); await SeedConfigAsync(listId, model: "opus"); await hub.UpdateListConfig(new UpdateListConfigDto(listId, null, null, null)); Assert.Null(await GetConfigAsync(listId)); } }