using Jellyfin.Plugin.Multilang.Services; using Jellyfin.Plugin.Multilang.Services.Refresh; namespace Jellyfin.Plugin.Multilang.Tests; public sealed class RefreshCoordinatorTests { [Fact] public async Task QueuedItemsUseSourceTierThenWorkClassPriority() { var coordinator = new RefreshCoordinator(new ItemsProxyCache()); var activeStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var releaseActive = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var order = new List(); var active = coordinator.EnqueueAsync("active", RefreshSourceTier.Background, RefreshWorkClass.Series, RefreshJobType.Full, async (_, _) => { activeStarted.SetResult(); await releaseActive.Task; return true; }, CancellationToken.None); await activeStarted.Task; var series = coordinator.EnqueueAsync("series", RefreshSourceTier.Background, RefreshWorkClass.Series, RefreshJobType.Full, (_, _) => { order.Add("series"); return Task.FromResult(true); }, CancellationToken.None); var movie = coordinator.EnqueueAsync("movie", RefreshSourceTier.Background, RefreshWorkClass.Movies, RefreshJobType.Full, (_, _) => { order.Add("movie"); return Task.FromResult(true); }, CancellationToken.None); var onTheFlyCollection = coordinator.EnqueueAsync("collection", RefreshSourceTier.OnTheFly, RefreshWorkClass.Collections, RefreshJobType.Full, (_, _) => { order.Add("collection"); return Task.FromResult(true); }, CancellationToken.None); releaseActive.SetResult(); await Task.WhenAll(active, series, movie, onTheFlyCollection); Assert.Equal(["collection", "movie", "series"], order); } [Fact] public async Task DuplicateQueuedItemUsesTheHighestJobAndTierAndInvalidatesCache() { var cache = new ItemsProxyCache(); Assert.True(cache.Store("target", "user", "/Items", ["target"], "body", "application/json", 1, 1024, TimeSpan.FromMinutes(1))); var coordinator = new RefreshCoordinator(cache); var activeStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var releaseActive = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var runs = new List(); var active = coordinator.EnqueueAsync("active", RefreshSourceTier.Background, RefreshWorkClass.Series, RefreshJobType.Full, async (_, _) => { activeStarted.SetResult(); await releaseActive.Task; return true; }, CancellationToken.None); await activeStarted.Task; var missing = coordinator.EnqueueAsync("target", RefreshSourceTier.Background, RefreshWorkClass.Series, RefreshJobType.Missing, (job, _) => { runs.Add(job); return Task.FromResult(true); }, CancellationToken.None); var full = coordinator.EnqueueAsync("target", RefreshSourceTier.Manual, RefreshWorkClass.Series, RefreshJobType.Full, (job, _) => { runs.Add(job); return Task.FromResult(true); }, CancellationToken.None); Assert.Same(missing, full); releaseActive.SetResult(); await Task.WhenAll(active, full); Assert.Equal([RefreshJobType.Full], runs); Assert.False(cache.TryGet("target", TimeSpan.FromMinutes(1), out _)); var recent = coordinator.GetDiagnostics().Recent.Single(item => item.ItemId == "target"); Assert.Equal("Manual", recent.SourceTier); Assert.Equal("Full", recent.JobType); } }