using LiveRecorder.Domain.Entities; using LiveRecorder.Domain.Enums; using LiveRecorder.Infrastructure.Services; using System.Xml.Linq; namespace LiveRecorder.Tests; public sealed class RecordingContinuityTests { [Theory] [InlineData(true, false, true)] [InlineData(false, true, true)] [InlineData(false, false, false)] public void SessionTransition_IsTreatedAsActive(bool process, bool transition, bool expected) => Assert.Equal(expected, FfmpegService.IsSessionRuntimeActive(process, transition)); [Theory] [InlineData(1, 5)] [InlineData(2, 15)] [InlineData(3, 30)] [InlineData(4, 60)] [InlineData(5, 120)] [InlineData(50, 120)] public void RuntimeRecoveryBackoff_IsBoundedAtTwoMinutes(int attempt, int seconds) => Assert.Equal(TimeSpan.FromSeconds(seconds), FfmpegService.GetRuntimeRecoveryDelay(attempt)); [Theory] [InlineData(null, false, false)] [InlineData(0.0, false, false)] [InlineData(0.1, true, false)] [InlineData(4.99, true, false)] [InlineData(5.0, true, true)] public void MediaDuration_SeparatesReadableFromStandalone(double? duration, bool readable, bool standalone) { Assert.Equal(readable, FfmpegService.IsReadableMediaDuration(duration)); Assert.Equal(standalone, FfmpegService.IsStandaloneMediaDuration(duration)); } [Fact] public void OfflineSample_RequiresConfirmation_ButLiveDoesNot() { Assert.True(LiveRoomPollingBackgroundService.RequiresOfflineConfirmation(false)); Assert.False(LiveRoomPollingBackgroundService.RequiresOfflineConfirmation(true)); } [Theory] [InlineData(false, RecordTaskStatus.Starting, true)] [InlineData(false, RecordTaskStatus.Running, true)] [InlineData(true, RecordTaskStatus.Running, false)] [InlineData(false, RecordTaskStatus.Completed, false)] public void ArtifactlessRecovery_ReusesOnlyActiveTask(bool hasMedia, RecordTaskStatus status, bool expected) => Assert.Equal(expected, FfmpegService.ShouldReuseRecoveryTask(hasMedia, status)); [Fact] public void SessionRecoveryMarker_KeepsSessionActive() { var session = new RecordSession(Guid.NewGuid(), "origin", RecordOutputFormat.Mp4, RecordSaveMode.Segmented, DateTimeOffset.UtcNow); session.MarkRecovering("[runtime-recovery] attempt=4; curl EOF", DateTimeOffset.UtcNow); Assert.Equal(RecordSessionStatus.Starting, session.Status); Assert.StartsWith("[runtime-recovery]", session.ErrorMessage); Assert.Null(session.EndedAt); } [Fact] public void MergedSource_PointsAtTargetAndBecomesHidden() { var source = new RecordTask(Guid.NewGuid(), Guid.NewGuid(), 2, "origin", RecordOutputFormat.Mp4, DateTimeOffset.UtcNow); var targetId = Guid.NewGuid(); source.MarkMergedSource(targetId, DateTimeOffset.UtcNow); Assert.True(source.IsHiddenArtifactSource); Assert.Equal(targetId, source.MergedIntoRecordTaskId); } [Fact] public void MergeWindow_FirstShortFragment_MergesForward() { var window = ShortFragmentConsolidationService.ResolveMergeWindow([1.2, 60], 0); Assert.Equal((0, 1), window); } [Fact] public void MergeWindow_TrailingShortFragment_MergesBackward() { var window = ShortFragmentConsolidationService.ResolveMergeWindow([60, 1.2], 0); Assert.Equal((0, 1), window); } [Fact] public void MergeWindow_ConsecutiveShortFragments_AreAccumulated() { var window = ShortFragmentConsolidationService.ResolveMergeWindow([1.2, 2.4, 60], 1); Assert.Equal((0, 2), window); } [Fact] public void DanmakuMerge_ShiftsRelativeOffset_ButPreservesAbsoluteTimestamp() { var chat = XElement.Parse("hi"); var shiftedChat = ShortFragmentConsolidationService.AdjustDanmakuElementOffsets(chat, 4.25); Assert.StartsWith("5.8,", shiftedChat.Attribute("p")!.Value); Assert.Contains("1720000000000", shiftedChat.Attribute("p")!.Value); var eventElement = XElement.Parse(""); var shiftedEvent = ShortFragmentConsolidationService.AdjustDanmakuElementOffsets(eventElement, 4.25); Assert.Equal("6.2", shiftedEvent.Attribute("offset")!.Value); Assert.Equal("1720000000123", shiftedEvent.Attribute("ts")!.Value); } }