using MBN_STOCK_WEBVIEW.Playout.Runtime; namespace MBN_STOCK_WEBVIEW.Playout.Tests; public sealed class DryRunPlayoutEngineTests { [Fact] public async Task DefaultOptions_AreDryRunAndNeverReportARealConnection() { await using var engine = CreateEngine(PlayoutMode.DryRun); var result = await engine.ConnectAsync(CancellationToken.None); Assert.Equal(PlayoutMode.DryRun, engine.Status.Mode); Assert.Equal(PlayoutConnectionState.DryRunReady, engine.Status.State); Assert.True(engine.Status.IsCommandAvailable); Assert.False(engine.Status.IsConnected); Assert.Equal( PlayoutKtapConnectState.NotAttempted, engine.Status.LastKtapConnectState); Assert.False(engine.Status.KtapConnectAttempted); Assert.False(engine.Status.NetworkMonitoringRecordExpected); Assert.False(engine.Status.NetworkMonitoringCheckRequired); Assert.Null(engine.Status.KtapHelloObserved); Assert.Contains("Network Monitoring", engine.Status.Message, StringComparison.Ordinal); Assert.True(result.IsSuccess); Assert.True(result.IsDryRun); } [Fact] public async Task DisabledMode_RejectsEveryOperationWithoutChangingState() { await using var engine = CreateEngine(PlayoutMode.Disabled); var cue = Cue("disabled-scene"); var results = new[] { await engine.ConnectAsync(CancellationToken.None), await engine.PrepareAsync(cue, CancellationToken.None), await engine.TakeInAsync(CancellationToken.None), await engine.NextAsync(cue, CancellationToken.None), await engine.UpdateOnAirAsync(cue, CancellationToken.None), await engine.TakeOutAsync( PlayoutTakeOutScope.All, CancellationToken.None), await engine.DisconnectAsync(CancellationToken.None) }; Assert.All(results, result => Assert.Equal(PlayoutResultCode.Rejected, result.Code)); Assert.All(results, result => Assert.False(result.IsDryRun)); Assert.Equal(PlayoutConnectionState.Disabled, engine.Status.State); Assert.False(engine.Status.IsConnected); Assert.False(engine.Status.IsCommandAvailable); Assert.Null(engine.Status.PreparedSceneName); Assert.Null(engine.Status.OnAirSceneName); } [Fact] public async Task DryRun_TransitionsPreparedAndOnAirSceneStateInCommandOrder() { await using var engine = CreateEngine(PlayoutMode.DryRun); var statuses = new List(); engine.StatusChanged += (_, args) => statuses.Add(args.Current); var connect = await engine.ConnectAsync(CancellationToken.None); var prepare = await engine.PrepareAsync( Cue("first-scene"), CancellationToken.None); Assert.Equal("first-scene", engine.Status.PreparedSceneName); Assert.Null(engine.Status.OnAirSceneName); var takeIn = await engine.TakeInAsync(CancellationToken.None); Assert.Null(engine.Status.PreparedSceneName); Assert.Equal("first-scene", engine.Status.OnAirSceneName); var update = await engine.UpdateOnAirAsync( Cue("first-scene"), CancellationToken.None); Assert.Null(engine.Status.PreparedSceneName); Assert.Equal("first-scene", engine.Status.OnAirSceneName); var next = await engine.NextAsync( Cue("next-scene"), CancellationToken.None); Assert.Null(engine.Status.PreparedSceneName); Assert.Equal("next-scene", engine.Status.OnAirSceneName); var takeOut = await engine.TakeOutAsync( PlayoutTakeOutScope.Layout, CancellationToken.None); Assert.Null(engine.Status.OnAirSceneName); var disconnect = await engine.DisconnectAsync(CancellationToken.None); Assert.All( new[] { connect, prepare, takeIn, update, next, takeOut, disconnect }, result => Assert.Equal(PlayoutResultCode.Success, result.Code)); Assert.All( new[] { connect, prepare, takeIn, update, next, takeOut, disconnect }, result => Assert.True(result.IsDryRun)); Assert.Equal(7, statuses.Count); Assert.True(statuses.Zip(statuses.Skip(1), (left, right) => left.Sequence < right.Sequence).All(x => x)); Assert.All(statuses, status => Assert.Equal(PlayoutConnectionState.DryRunReady, status.State)); } [Fact] public async Task DryRun_TakeInWithoutPreparedSceneIsRejected() { await using var engine = CreateEngine(PlayoutMode.DryRun); var result = await engine.TakeInAsync(CancellationToken.None); Assert.Equal(PlayoutResultCode.Rejected, result.Code); Assert.Null(engine.Status.OnAirSceneName); } [Fact] public async Task DryRun_NextAfterPrepareButBeforeTakeInIsRejected() { await using var engine = CreateEngine(PlayoutMode.DryRun); var idleNext = await engine.NextAsync( Cue("next-scene"), CancellationToken.None); Assert.Equal(PlayoutResultCode.Rejected, idleNext.Code); Assert.Null(engine.Status.PreparedSceneName); Assert.Null(engine.Status.OnAirSceneName); Assert.True((await engine.PrepareAsync( Cue("first-scene"), CancellationToken.None)).IsSuccess); var next = await engine.NextAsync( Cue("next-scene"), CancellationToken.None); Assert.Equal(PlayoutResultCode.Rejected, next.Code); Assert.Equal("first-scene", engine.Status.PreparedSceneName); Assert.Null(engine.Status.OnAirSceneName); } [Fact] public async Task CancelledDryRunCommand_DoesNotChangeStatus() { await using var engine = CreateEngine(PlayoutMode.DryRun); var initialStatus = engine.Status; using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); var result = await engine.PrepareAsync(Cue("cancelled-scene"), cancellation.Token); Assert.Equal(PlayoutResultCode.Cancelled, result.Code); Assert.Same(initialStatus, engine.Status); Assert.Null(engine.Status.PreparedSceneName); } [Fact] public async Task DryRun_PublicStatusAndResultDoNotExposeScenePath() { const string secretPath = "C:\\private-vendor-assets\\do-not-log\\secret.t2s"; await using var engine = CreateEngine(PlayoutMode.DryRun); var cue = new PlayoutCue(secretPath, "C:\\unsafe-scene-name"); var result = await engine.PrepareAsync(cue, CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal("장면", engine.Status.PreparedSceneName); Assert.DoesNotContain(secretPath, result.Message, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain(secretPath, engine.Status.Message, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain(cue.SceneName, result.Message, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain(cue.SceneName, engine.Status.Message, StringComparison.OrdinalIgnoreCase); } private static PlayoutCue Cue(string sceneName) => new( $"C:\\test-only\\{sceneName}.t2s", sceneName, [new PlayoutField("headline", "safe value", true)]); private static TornadoPlayoutEngine CreateEngine(PlayoutMode mode) { var options = ValidatedPlayoutOptions.Create(new PlayoutOptions { Mode = mode }); return new TornadoPlayoutEngine( options, new FakeK3dSessionFactory(), new FakeRegistrationProbe(), new FakeTornadoProcessProbe(new TornadoProcessSnapshot(0, 0, 0)), null, new FakeLiveAuthorization(false), TimeProvider.System, startMonitor: false); } }