using System.Text; using Data.Data.Repositories; using Data.Managers; using DeepDrftAPI.Services; using DeepDrftContent; using DeepDrftContent.Constants; using DeepDrftContent.FileDatabase.Models; using DeepDrftContent.Processors; using DeepDrftContent.Processors.Opus; using DeepDrftData; using DeepDrftData.Data; using DeepDrftData.Repositories; using DeepDrftModels.DTOs; using DeepDrftModels.Entities; using DeepDrftModels.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase; namespace DeepDrftTests; /// /// Tests for the Phase 18.5 Backfill-Opus scheduling contract /// ( and ). /// These assert the enqueue decision — which tracks get a background derive scheduled — over a real /// , a real , and an in-memory SQL store, with a /// recording standing in for the background worker (the actual transcode /// is not exercised here — it needs ffmpeg and is out of scope for the scheduling contract). /// /// The decision under test: a track is enqueued iff it lacks a COMPLETE Opus artifact (both the Opus audio /// bytes and the seek/setup sidecar). A track with both is skipped; a half-derived track (audio without /// sidecar) is treated as incomplete and re-enqueued so a backfill heals it. /// [TestFixture] public class OpusBackfillTests { private string _testDir = string.Empty; private DeepDrftContext _context = null!; [SetUp] public void SetUp() { _testDir = Path.Combine(Path.GetTempPath(), "OpusBackfillTests", Guid.NewGuid().ToString()); Directory.CreateDirectory(_testDir); var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; _context = new DeepDrftContext(options); } [TearDown] public void TearDown() { _context.Dispose(); try { Directory.Delete(_testDir, recursive: true); } catch { /* Best-effort cleanup — ignore failures */ } } private TrackManager CreateManager() { var repository = new TrackRepository( _context, NullLogger>.Instance); return new TrackManager( repository, NullLogger>.Instance); } private sealed record Harness( UnifiedTrackService Service, NoOpOpusTranscodeQueue Queue, TrackContentService Content, FileDb FileDatabase, ITrackService Sql); private async Task BuildAsync() { var fileDatabase = await FileDb.FromAsync(_testDir); Assert.That(fileDatabase, Is.Not.Null); var content = new TrackContentService( fileDatabase!, new AudioProcessorRouter( new AudioProcessor(), new Mp3AudioProcessor(), new FlacAudioProcessor())); var waveforms = new WaveformProfileService( fileDatabase!, new AudioProcessor(), new RmsLoudnessAlgorithm(), Options.Create(new WaveformProfileOptions()), NullLogger.Instance); var resolver = new TrackFormatResolver( fileDatabase!, content, NullLogger.Instance); var queue = new NoOpOpusTranscodeQueue(); var sql = CreateManager(); var service = new UnifiedTrackService( content, sql, fileDatabase!, waveforms, queue, resolver, NullLogger.Instance); await fileDatabase!.CreateVaultAsync(VaultConstants.TrackOpus, MediaVaultType.Audio); return new Harness(service, queue, content, fileDatabase!, sql); } // Seeds a track: stores a real source WAV in the tracks vault and a SQL row pointing at the same EntryKey. // Returns the EntryKey so the test can selectively add Opus artifacts to a subset. private async Task SeedTrackAsync(Harness h, string title) { var wavPath = Path.Combine(_testDir, Guid.NewGuid().ToString("N") + ".wav"); await File.WriteAllBytesAsync(wavPath, BuildMinimalPcmWav(2.0)); var unpersisted = await h.Content.AddTrackAsync(wavPath, title, "Artist"); Assert.That(unpersisted, Is.Not.Null); var dto = new TrackDto { EntryKey = unpersisted!.EntryKey, TrackName = title }; var created = await h.Sql.Create(dto); Assert.That(created.Success, Is.True, created.Messages.FirstOrDefault()?.Message); return unpersisted.EntryKey; } private async Task StoreOpusAudioAsync(Harness h, string entryKey) { var opus = new AudioBinary(new AudioBinaryParams("opus"u8.ToArray(), 4, ".opus", 2.0, 320)); Assert.That( await h.FileDatabase.RegisterResourceAsync( VaultConstants.TrackOpus, OpusTranscodeService.OpusAudioKey(entryKey), opus), Is.True); } private async Task StoreSidecarAsync(Harness h, string entryKey) { var sidecar = new MediaBinary(new MediaBinaryParams("idx"u8.ToArray(), 3, ".opusidx")); Assert.That( await h.FileDatabase.RegisterResourceAsync( VaultConstants.TrackOpus, OpusTranscodeService.OpusSidecarKey(entryKey), sidecar), Is.True); } [Test] public async Task BackfillOpus_EnqueuesOnlyTracksWithoutCompleteOpus() { var h = await BuildAsync(); // Three tracks: one fully derived (audio + sidecar), one bare (no Opus), one half-derived (audio only). var complete = await SeedTrackAsync(h, "Complete"); await StoreOpusAudioAsync(h, complete); await StoreSidecarAsync(h, complete); var bare = await SeedTrackAsync(h, "Bare"); var halfDerived = await SeedTrackAsync(h, "HalfDerived"); await StoreOpusAudioAsync(h, halfDerived); // audio but no sidecar → unseekable → treated as incomplete var result = await h.Service.BackfillOpusAsync(CancellationToken.None); Assert.That(result.Success, Is.True, result.Messages.FirstOrDefault()?.Message); Assert.Multiple(() => { Assert.That(result.Value.Enqueued, Is.EqualTo(2), "the bare and half-derived tracks must be enqueued"); Assert.That(result.Value.Skipped, Is.EqualTo(1), "the fully-derived track must be skipped"); Assert.That(h.Queue.Enqueued, Does.Contain(bare)); Assert.That(h.Queue.Enqueued, Does.Contain(halfDerived)); Assert.That(h.Queue.Enqueued, Does.Not.Contain(complete), "a complete Opus artifact is not re-enqueued"); }); } [Test] public async Task BackfillOpus_WhenAllTracksHaveOpus_EnqueuesNothing() { var h = await BuildAsync(); var a = await SeedTrackAsync(h, "A"); await StoreOpusAudioAsync(h, a); await StoreSidecarAsync(h, a); var result = await h.Service.BackfillOpusAsync(CancellationToken.None); Assert.That(result.Success, Is.True); Assert.Multiple(() => { Assert.That(result.Value.Enqueued, Is.Zero); Assert.That(result.Value.Skipped, Is.EqualTo(1)); Assert.That(h.Queue.Enqueued, Is.Empty, "an all-derived catalogue schedules no transcodes"); }); } [Test] public async Task EnqueueOpus_KnownTrack_Enqueues() { var h = await BuildAsync(); var entryKey = await SeedTrackAsync(h, "Solo"); var result = await h.Service.EnqueueOpusAsync(entryKey, CancellationToken.None); Assert.That(result.Success, Is.True); Assert.That(h.Queue.Enqueued, Does.Contain(entryKey)); } [Test] public async Task EnqueueOpus_UnknownTrack_FailsWithNotFound_AndEnqueuesNothing() { var h = await BuildAsync(); var result = await h.Service.EnqueueOpusAsync("no-such-track", CancellationToken.None); Assert.Multiple(() => { Assert.That(result.Success, Is.False); Assert.That(result.Messages.FirstOrDefault()?.Message, Is.EqualTo(UnifiedTrackService.TrackNotFoundMessage)); Assert.That(h.Queue.Enqueued, Is.Empty, "an unknown track must not schedule a transcode"); }); } // Standard-PCM mono 16-bit 44.1 kHz WAV, full-scale square wave. Same layout as the other suites. private static byte[] BuildMinimalPcmWav(double durationSeconds) { const int sampleRate = 44100; const ushort channels = 1; const ushort bitsPerSample = 16; const ushort blockAlign = channels * (bitsPerSample / 8); const uint byteRate = sampleRate * blockAlign; var frames = (int)(sampleRate * durationSeconds); var data = new byte[frames * blockAlign]; for (var i = 0; i < frames; i++) { var sample = (i % 2 == 0) ? short.MaxValue : short.MinValue; data[i * 2] = (byte)(sample & 0xFF); data[i * 2 + 1] = (byte)((sample >> 8) & 0xFF); } using var ms = new MemoryStream(); using var w = new BinaryWriter(ms, Encoding.ASCII, leaveOpen: true); w.Write(Encoding.ASCII.GetBytes("RIFF")); w.Write((uint)(36 + data.Length)); w.Write(Encoding.ASCII.GetBytes("WAVE")); w.Write(Encoding.ASCII.GetBytes("fmt ")); w.Write(16u); w.Write((ushort)1); // PCM w.Write(channels); w.Write((uint)sampleRate); w.Write(byteRate); w.Write(blockAlign); w.Write(bitsPerSample); w.Write(Encoding.ASCII.GetBytes("data")); w.Write((uint)data.Length); w.Write(data); w.Flush(); return ms.ToArray(); } }