namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests; public sealed class LegacyPlayoutNativeContractTests { private static readonly string RepositoryRoot = FindRepositoryRoot(); private static readonly string AppRoot = Path.Combine( RepositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp"); private static readonly string Playout = File.ReadAllText( Path.Combine(AppRoot, "MainWindow.Playout.cs")); private static readonly string Window = File.ReadAllText( Path.Combine(AppRoot, "MainWindow.xaml.cs")); private static readonly string Project = File.ReadAllText( Path.Combine(AppRoot, "MBN_STOCK_WEBVIEW.LegacyParityApp.csproj")); [Fact] public void ParityAppComposesTheVerifiedThirtyFiveSceneRuntime() { Assert.Contains("LegacySceneRuntimeFactory.cs", Project, StringComparison.Ordinal); Assert.Contains("LegacySceneRuntimeFactory.Create(", Playout, StringComparison.Ordinal); Assert.Contains("trustedManualSource: _manualNetSellStore", Playout, StringComparison.Ordinal); Assert.Contains("trustedViSource: _manualViStore", Playout, StringComparison.Ordinal); } [Fact] public void StartupDefaultBackgroundDependsOnAbsentLocalConfigAndSurfacesWarning() { Assert.Contains("File.Exists(", Playout, StringComparison.Ordinal); Assert.Contains("PlayoutOptionsLoader.DefaultPath", Playout, StringComparison.Ordinal); Assert.Contains("LegacyParityStartupCompositionResolver.Resolve(", Playout, StringComparison.Ordinal); Assert.Contains("_playoutInitializationWarning ?? status?.Message", Playout, StringComparison.Ordinal); } [Fact] public void OperatorCommandsUseTheNativeWorkflowAndCSharpPlaylist() { Assert.Contains("_controller.CreatePlayoutRequest(", Playout, StringComparison.Ordinal); Assert.Contains("_compositionOptions!.Current.FadeDuration", Playout, StringComparison.Ordinal); Assert.Contains("workflow.PrepareAsync(", Playout, StringComparison.Ordinal); Assert.Contains("workflow.TakeSelectedAsync(", Playout, StringComparison.Ordinal); Assert.DoesNotContain("workflow.TakeInAsync(", Playout, StringComparison.Ordinal); Assert.DoesNotContain( "string.IsNullOrWhiteSpace(workflow.State.PreparedCutCode)", Playout, StringComparison.Ordinal); Assert.Contains("workflow.NextAsync(", Playout, StringComparison.Ordinal); Assert.Contains("workflow.TakeOutAsync(", Playout, StringComparison.Ordinal); Assert.Contains("_controller.ReflectSuccessfulPlayout(", Playout, StringComparison.Ordinal); Assert.Contains("workflow.State.CurrentEntryId", Playout, StringComparison.Ordinal); Assert.Contains("LegacyOperatorPlayoutCommand.Prepare or", Playout, StringComparison.Ordinal); Assert.Contains( "command != LegacyOperatorPlayoutCommand.TakeOut", Playout, StringComparison.Ordinal); Assert.Contains("LegacyWorkflowNextKind.PlaylistNext", Playout, StringComparison.Ordinal); Assert.Contains("LegacySceneRefreshPolicy.TryGetInterval", Playout, StringComparison.Ordinal); Assert.Contains("WaitForPlayCompletionAsync", Playout, StringComparison.Ordinal); } [Fact] public void PlayoutIntentsWaitBehindEarlierUiWorkWithoutQueuingDuplicates() { var process = Normalize(Slice( Window, "private async Task ProcessIntentAsync(LegacyUiIntent intent)", "private static bool IsOrderedPlayoutIntent")); var classifier = Slice( Window, "private static bool IsOrderedPlayoutIntent", "private static bool IsNamedPlaylistModalIntent"); var admission = RequiredIndex( process, "_orderedPlayoutIntentGate.WaitAsync("); var orderedWait = RequiredIndex( process, "await _intentGate.WaitAsync(_lifetimeCancellation.Token)", admission); var zeroTimeoutWait = RequiredIndex( process, "gateEntered = await _intentGate.WaitAsync(\n 0,", orderedWait); var intentRelease = RequiredIndex(process, "_intentGate.Release();", zeroTimeoutWait); var admissionRelease = RequiredIndex( process, "_orderedPlayoutIntentGate.Release();", intentRelease); Assert.True(admission < orderedWait); Assert.True(orderedWait < zeroTimeoutWait); Assert.True(zeroTimeoutWait < intentRelease); Assert.True(intentRelease < admissionRelease); Assert.Contains("if (orderedPlayoutIntentEntered)", process, StringComparison.Ordinal); Assert.Contains("LegacyExecutePlayoutIntent", classifier, StringComparison.Ordinal); Assert.Contains("LegacyGateAPrepareIntent", classifier, StringComparison.Ordinal); Assert.Contains("_lifetimeCancellation.Token", process, StringComparison.Ordinal); } [Fact] public void AutomaticRefreshUsesTheCoreSchedulerAndGenerationOwnedState() { Assert.Contains("new LegacyRefreshScheduler(", Playout, StringComparison.Ordinal); Assert.Contains( "LegacyRefreshEpoch", Playout, StringComparison.Ordinal); Assert.Contains("_refreshEpoch.ReadState()", Playout, StringComparison.Ordinal); Assert.Contains("_refreshEpoch.Replace(", Playout, StringComparison.Ordinal); Assert.Contains("_refreshEpoch.TryUpdate(", Playout, StringComparison.Ordinal); Assert.Contains("_refreshEpoch.TryComplete(", Playout, StringComparison.Ordinal); Assert.DoesNotContain("Task.Delay(", Playout, StringComparison.Ordinal); foreach (var obsoleteRawField in new[] { "_refreshCancellation", "_refreshCts", "_refreshStateGate", "_refreshActive", "_refreshCompletedCount", "_refreshMaximumCount", "_refreshLimitReached", "_refreshNextAtUtc", "_refreshLastSuccessAtUtc", "_refreshFaultCode", "_refreshMessage" }) { Assert.DoesNotContain(obsoleteRawField, Playout, StringComparison.Ordinal); } } [Fact] public void ZeroRefreshLimitReturnsBeforeCreatingAnEpochOrLoop() { var start = Slice( Playout, "private void StartRefreshLoop(LegacyPlayoutWorkflow workflow)", "private void StopRefreshLoop()"); var limitCheck = RequiredIndex(start, "if (scheduler.HasReachedMaximum)"); var linkedEpoch = RequiredIndex( start, "CancellationTokenSource.CreateLinkedTokenSource("); var loopAssignment = RequiredIndex( start, "_refreshTask = RunRefreshLoopAsync(workflow, scheduler, cancellation);"); Assert.True(limitCheck < linkedEpoch); Assert.True(linkedEpoch < loopAssignment); var zeroLimitBranch = start[limitCheck..linkedEpoch]; Assert.Contains("_refreshTask = Task.CompletedTask;", zeroLimitBranch, StringComparison.Ordinal); Assert.Contains("_refreshEpoch.Stop(", zeroLimitBranch, StringComparison.Ordinal); Assert.Contains("return;", zeroLimitBranch, StringComparison.Ordinal); Assert.DoesNotContain("RunRefreshLoopAsync(", zeroLimitBranch, StringComparison.Ordinal); } [Fact] public void AutomaticRefreshSerializesDbThenPlayoutAndPublishesBothBusyStates() { var loop = Slice( Playout, "private async Task RunRefreshLoopAsync(", "private bool TrySetRefreshFault("); var intentWait = RequiredIndex(loop, "await _intentGate.WaitAsync(token)"); var intentBusy = RequiredIndex(loop, "_intentBusy = true;", intentWait); var intentBusyState = RequiredIndex(loop, "QueueOperatorState();", intentBusy); var playoutWait = RequiredIndex( loop, "await _playoutCommandGate.WaitAsync(token)", intentBusyState); var playoutBusy = RequiredIndex( loop, "Interlocked.Exchange(ref _playoutBusy, 1);", playoutWait); var playoutBusyState = RequiredIndex(loop, "QueueOperatorState();", playoutBusy); var refreshDispatch = RequiredIndex( loop, "workflow.RefreshOnAirAsync", playoutBusyState); Assert.True(intentWait < intentBusy); Assert.True(intentBusy < intentBusyState); Assert.True(intentBusyState < playoutWait); Assert.True(playoutWait < playoutBusy); Assert.True(playoutBusy < playoutBusyState); Assert.True(playoutBusyState < refreshDispatch); } [Fact] public void KnownRefreshInputFailuresStopWithoutQuarantineButUnknownsDoQuarantine() { var loop = Normalize(Slice( Playout, "private async Task RunRefreshLoopAsync(", "private bool TrySetRefreshFault(")); var databaseFailure = Slice( loop, "catch (DatabaseInfrastructureException)", "catch (LegacySceneDataException)"); var sceneDataFailure = Slice( loop, "catch (LegacySceneDataException)", "catch (ArgumentException)"); var argumentFailure = Slice( loop, "catch (ArgumentException)", "catch\n {"); var unknownFailure = Slice( loop, "catch\n {", "finally"); AssertKnownFailureDoesNotQuarantine( databaseFailure, "DATABASE_UNAVAILABLE"); AssertKnownFailureDoesNotQuarantine( sceneDataFailure, "SCENE_DATA_INVALID"); AssertKnownFailureDoesNotQuarantine( argumentFailure, "SCENE_ARGUMENT_INVALID"); Assert.Contains("REFRESH_OUTCOME_UNKNOWN", unknownFailure, StringComparison.Ordinal); Assert.Contains("QuarantinePlayoutAsync", unknownFailure, StringComparison.Ordinal); Assert.Contains( "result.Code is PlayoutResultCode.OutcomeUnknown or\n" + " PlayoutResultCode.TimedOut", loop, StringComparison.Ordinal); AssertEveryFaultBranchQuarantines(loop, "PLAY_CALLBACK_TIMEOUT"); } [Fact] public void ExternalIdleStatusResetsNormalRefreshState() { var handler = Slice( Playout, "private void OnPlayoutStatusChanged(", "private async Task ExecuteOperatorPlayoutAsync("); Assert.Contains( "string.IsNullOrWhiteSpace(args.Current.PreparedSceneName)", handler, StringComparison.Ordinal); Assert.Contains( "string.IsNullOrWhiteSpace(args.Current.OnAirSceneName)", handler, StringComparison.Ordinal); Assert.Contains( "args.Current.State == PlayoutConnectionState.OutcomeUnknown", handler, StringComparison.Ordinal); var stop = RequiredIndex(handler, "StopRefreshLoop();"); var normalElse = RequiredIndex(handler, "else", stop); var reset = RequiredIndex(handler, "ResetRefreshState();", normalElse); Assert.True(stop < normalElse && normalElse < reset); var resetHelper = Slice( Playout, "private void ResetRefreshState()", "private async Task RunRefreshLoopAsync("); Assert.Contains( "_refreshEpoch.Stop(_ => LegacyRefreshRuntimeStatus.Empty", resetHelper, StringComparison.Ordinal); } [Fact] public void UnknownOutcomeQuarantinesWithoutAutomaticCommandRetry() { Assert.Contains("PlayoutResultCode.OutcomeUnknown", Playout, StringComparison.Ordinal); Assert.Contains("PlayoutResultCode.TimedOut", Playout, StringComparison.Ordinal); Assert.Contains("QuarantinePlayoutAsync", Playout, StringComparison.Ordinal); Assert.DoesNotContain("MaximumReconnectAttempts", Playout, StringComparison.Ordinal); Assert.DoesNotContain("ReconnectAsync", Playout, StringComparison.Ordinal); } [Fact] public void OriginalDissolveComboAcceptsOnlyItsZeroBasedTwentyItemIndex() { Assert.Contains("fadeDuration is < 0 or > 19", Playout, StringComparison.Ordinal); Assert.DoesNotContain("fadeDuration is < 0 or > 60", Playout, StringComparison.Ordinal); } [Fact] public void HomeAndEndStopAutomaticRefreshBeforeChangingTheCandidateRow() { Assert.Contains( "LegacySelectPlaylistBoundaryIntent boundary =>\n" + " SelectPlaylistBoundaryAndStopRefresh(boundary.Last)", Window.Replace("\r\n", "\n", StringComparison.Ordinal), StringComparison.Ordinal); Assert.Contains( "private LegacyOperatorSnapshot SelectPlaylistBoundaryAndStopRefresh(bool last)", Playout, StringComparison.Ordinal); var helperStart = Playout.IndexOf( "private LegacyOperatorSnapshot SelectPlaylistBoundaryAndStopRefresh(bool last)", StringComparison.Ordinal); var helperEnd = Playout.IndexOf( "private async Task ToggleBackgroundAsync", helperStart, StringComparison.Ordinal); Assert.True(helperStart >= 0 && helperEnd > helperStart); var helper = Playout[helperStart..helperEnd]; Assert.True( helper.IndexOf("StopRefreshLoop();", StringComparison.Ordinal) < helper.IndexOf("_controller.SelectPlaylistBoundary(last)", StringComparison.Ordinal)); } [Fact] public void PreparedProgramBlocksCutAppendButAllowsPureCutSelection() { Assert.Contains("CanAcceptOperatorMutation()", Window, StringComparison.Ordinal); Assert.Contains("IsOperatorMutationQuarantined()", Window, StringComparison.Ordinal); Assert.Contains("ObserveOperatorMutationQuarantine(state)", Window, StringComparison.Ordinal); Assert.Contains("ManualFinancial?.WritesQuarantined", Playout, StringComparison.Ordinal); Assert.Contains("NamedPlaylist?.IsWriteQuarantined", Playout, StringComparison.Ordinal); Assert.Contains("ManualLists?.WritesQuarantined", Playout, StringComparison.Ordinal); Assert.Contains("OperatorCatalog?.WritesQuarantined", Playout, StringComparison.Ordinal); Assert.Contains("LegacyConfirmManualListIntent", Playout, StringComparison.Ordinal); var mutationClassifier = Slice( Playout, "private static bool IsOperatorMutationIntent", "private static bool IsFixedSectionBatchMutationIntent"); Assert.Contains("LegacyCutPointerDownIntent { AllowAppend: true }", mutationClassifier, StringComparison.Ordinal); Assert.DoesNotContain("LegacyCutKeyDownIntent", mutationClassifier, StringComparison.Ordinal); Assert.DoesNotContain("LegacyClearCutSelectionIntent", mutationClassifier, StringComparison.Ordinal); Assert.Contains("pointer.AllowAppend", Window, StringComparison.Ordinal); } private static void AssertKnownFailureDoesNotQuarantine( string failureBlock, string expectedFaultCode) { Assert.Contains(expectedFaultCode, failureBlock, StringComparison.Ordinal); Assert.DoesNotContain( "QuarantinePlayoutAsync", failureBlock, StringComparison.Ordinal); } private static void AssertEveryFaultBranchQuarantines( string source, string faultCode) { var occurrence = 0; var count = 0; while ((occurrence = source.IndexOf( faultCode, occurrence, StringComparison.Ordinal)) >= 0) { var branchEnd = source.IndexOf("return;", occurrence, StringComparison.Ordinal); Assert.True(branchEnd > occurrence, $"{faultCode} branch has no return."); Assert.Contains( "QuarantinePlayoutAsync", source[occurrence..branchEnd], StringComparison.Ordinal); count++; occurrence = branchEnd + "return;".Length; } Assert.True(count > 0, $"No {faultCode} branch was found."); } private static string Slice(string source, string startMarker, string endMarker) { var start = RequiredIndex(source, startMarker); var end = RequiredIndex(source, endMarker, start + startMarker.Length); return source[start..end]; } private static int RequiredIndex(string source, string marker, int startIndex = 0) { var index = source.IndexOf(marker, startIndex, StringComparison.Ordinal); Assert.True(index >= 0, $"Required source marker was not found: {marker}"); return index; } private static string Normalize(string source) => source.Replace("\r\n", "\n", StringComparison.Ordinal); private static string FindRepositoryRoot() { var current = new DirectoryInfo(AppContext.BaseDirectory); while (current is not null) { if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln"))) { return current.FullName; } current = current.Parent; } throw new DirectoryNotFoundException("Repository root was not found."); } }