fix: restore legacy playout parity and live startup

This commit is contained in:
2026-07-29 14:07:36 +09:00
parent 80b50c80b3
commit b35c000796
31 changed files with 3356 additions and 382 deletions

View File

@@ -73,35 +73,41 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
[Fact]
public void AppAppliesRuntimeGateBeforeConstructingAnyRuntimeWindow()
{
var app = File.ReadAllText(Path.Combine(ParityRoot, "App.xaml.cs"));
var app = File.ReadAllText(Path.Combine(ParityRoot, "App.xaml.cs"))
.ReplaceLineEndings("\n");
var bootstrap = app.IndexOf(
"DevelopmentLiveLaunchBootstrap.TryApply(",
StringComparison.Ordinal);
var constructSetupWindow = app.IndexOf(
"new FirstRunSetupWindow()",
StringComparison.Ordinal);
var constructMainWindow = app.IndexOf(
"new MainWindow();",
StringComparison.Ordinal);
var sourceOnlyDirective = app.IndexOf(
"#if SOURCE_ONLY_RUNTIME",
StringComparison.Ordinal);
var fullRuntimeDirective = app.IndexOf(
"#else",
constructSetupWindow,
sourceOnlyDirective,
StringComparison.Ordinal);
var runtimeEndDirective = app.IndexOf(
"#endif",
var preflight = app.IndexOf(
"ResolveDevelopmentLiveConfigurationRecoveryMessage(developmentLive)",
fullRuntimeDirective,
StringComparison.Ordinal);
var constructMainWindow = app.IndexOf(
"? new MainWindow()",
preflight,
StringComparison.Ordinal);
var constructRecoveryWindow = app.IndexOf(
": new FirstRunSetupWindow(",
constructMainWindow,
StringComparison.Ordinal);
var runtimeEndDirective = app.IndexOf("#endif", constructRecoveryWindow,
StringComparison.Ordinal);
Assert.True(bootstrap >= 0);
Assert.True(sourceOnlyDirective >= 0);
Assert.True(constructSetupWindow > sourceOnlyDirective);
Assert.True(fullRuntimeDirective > constructSetupWindow);
Assert.True(fullRuntimeDirective > sourceOnlyDirective);
Assert.True(bootstrap > fullRuntimeDirective);
Assert.True(constructMainWindow > bootstrap);
Assert.True(runtimeEndDirective > constructMainWindow);
Assert.True(preflight > bootstrap);
Assert.True(constructMainWindow > preflight);
Assert.True(constructRecoveryWindow > constructMainWindow);
Assert.True(runtimeEndDirective > constructRecoveryWindow);
Assert.Contains("args.Arguments", app, StringComparison.Ordinal);
Assert.Contains("Environment.GetCommandLineArgs()", app, StringComparison.Ordinal);
Assert.Contains("ResolveLaunchArguments(", app, StringComparison.Ordinal);
@@ -110,12 +116,71 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
app,
StringComparison.Ordinal);
Assert.Contains(
"IsDevelopmentLiveLaunch = developmentLive.IsApplied;",
"IsDevelopmentLiveLaunch =\n" +
" developmentLive.IsApplied &&\n" +
" configurationRecoveryMessage is null;",
app,
StringComparison.Ordinal);
Assert.DoesNotContain("#if DEBUG", app, StringComparison.Ordinal);
}
[Fact]
public void InvalidProtectedLiveProfileRoutesToExplicitRecoveryWithoutConstructingMainWindow()
{
var app = File.ReadAllText(Path.Combine(ParityRoot, "App.xaml.cs"))
.ReplaceLineEndings("\n");
Assert.Contains(
"_ = PlayoutOptionsLoader.LoadDevelopmentLive();",
app,
StringComparison.Ordinal);
Assert.Contains(
"catch (PlayoutConfigurationException)",
app,
StringComparison.Ordinal);
Assert.Contains(
"\"development-live-configuration-replacement-required\"",
app,
StringComparison.Ordinal);
Assert.Contains(
"버튼을 누르기 전에는 기존 Live 설정을 변경하지 않습니다.",
app,
StringComparison.Ordinal);
Assert.Contains(
"FirstRunSetupWindow.IsVerifiedSetupAvailable()",
app,
StringComparison.Ordinal);
Assert.Contains(
"liveConfigReplacementAvailable: recoverySetupAvailable",
app,
StringComparison.Ordinal);
Assert.Contains(
"현재 빌드에는 검증된 Git 소스 경로가 포함되어 있지 않아 자동 교체를 실행할 수 없습니다.",
app,
StringComparison.Ordinal);
var preflight = app.IndexOf(
"ResolveDevelopmentLiveConfigurationRecoveryMessage(developmentLive)",
StringComparison.Ordinal);
var windowChoice = app.IndexOf(
"_window = configurationRecoveryMessage is null",
preflight,
StringComparison.Ordinal);
var mainWindow = app.IndexOf(
"? new MainWindow()",
windowChoice,
StringComparison.Ordinal);
var recoveryWindow = app.IndexOf(
": new FirstRunSetupWindow(",
mainWindow,
StringComparison.Ordinal);
Assert.True(preflight >= 0);
Assert.True(windowChoice > preflight);
Assert.True(mainWindow > windowChoice);
Assert.True(recoveryWindow > mainWindow);
}
[Fact]
public void AppliedDevelopmentLiveUsesSelectedRuntimePathsAndSelectedResDatabaseAuthority()
{

View File

@@ -11,7 +11,7 @@ public sealed class LegacyFirstRunSetupNativeContractTests
"MBN_STOCK_WEBVIEW.LegacyParityApp");
[Fact]
public void SourceOnlyLaunchShowsOnlyTheNativeFirstRunWindow()
public void SourceOnlyLaunchUsesSetupAndFullRuntimeMayRecoverBeforeMainWindow()
{
var app = File.ReadAllText(Path.Combine(AppRoot, "App.xaml.cs"))
.ReplaceLineEndings("\n");
@@ -37,7 +37,7 @@ public sealed class LegacyFirstRunSetupNativeContractTests
Assert.Contains("DevelopmentLiveLaunchBootstrap.TryApply(", fullRuntime,
StringComparison.Ordinal);
Assert.Contains("new MainWindow()", fullRuntime, StringComparison.Ordinal);
Assert.DoesNotContain("new FirstRunSetupWindow()", fullRuntime,
Assert.Contains("new FirstRunSetupWindow(", fullRuntime,
StringComparison.Ordinal);
Assert.DoesNotContain(
"new FirstRunSetupWindow(App.IsDevelopmentLiveLaunch",
@@ -257,7 +257,78 @@ public sealed class LegacyFirstRunSetupNativeContractTests
}
[Fact]
public void SourceOnlyBuildEmbedsOnlyItsOwnClonePathForTheLocalBinding()
public void RecoveryPrefillsValidatedStoredFoldersAndRequiresExplicitReplacement()
{
var setup = File.ReadAllText(Path.Combine(
AppRoot,
"FirstRunSetupWindow.xaml.cs"))
.ReplaceLineEndings("\n");
Assert.Contains(
"RestoreStoredRuntimeFolders();",
setup,
StringComparison.Ordinal);
Assert.Contains(
"var loaded = new LegacyOperatorSettingsStore().Load();",
setup,
StringComparison.Ordinal);
Assert.Contains(
"loaded.Failure != LegacyOperatorSettingsStoreFailure.None",
setup,
StringComparison.Ordinal);
Assert.Contains(
"loaded.Settings.SceneDirectory",
setup,
StringComparison.Ordinal);
Assert.Contains(
"loaded.Settings.ResourceDirectory",
setup,
StringComparison.Ordinal);
Assert.Contains(
"_liveConfigReplacementAvailable =\n" +
" liveConfigReplacementAvailable &&\n" +
" IsVerifiedSetupAvailable();",
setup,
StringComparison.Ordinal);
Assert.Contains(
"liveConfigReplacementAvailable && _runtimeFoldersReady",
setup,
StringComparison.Ordinal);
Assert.Contains(
"await RunSetupFromUiAsync(replaceLiveConfig: true);",
setup,
StringComparison.Ordinal);
Assert.Contains(
"internal static bool IsVerifiedSetupAvailable() =>\n" +
" TryResolveRepositoryRoot(out _);",
setup,
StringComparison.Ordinal);
Assert.Contains(
"SingleOrDefault(attribute => string.Equals(",
setup,
StringComparison.Ordinal);
Assert.Contains(
"if (!File.Exists(project))",
setup,
StringComparison.Ordinal);
Assert.Contains(
"if (replaceLiveConfig)\n" +
" {\n" +
" AddArgument(\"-ReplaceLiveConfig\");",
setup,
StringComparison.Ordinal);
Assert.DoesNotContain(
"RunSetupFromUiAsync(replaceLiveConfig: true);",
setup[
setup.IndexOf("internal FirstRunSetupWindow(", StringComparison.Ordinal)..
setup.IndexOf(
"private async void OnChooseCutsClicked",
StringComparison.Ordinal)],
StringComparison.Ordinal);
}
[Fact]
public void DebugBuildEmbedsItsOwnClonePathForFirstRunAndRecovery()
{
var project = XDocument.Load(Path.Combine(
AppRoot,
@@ -266,14 +337,9 @@ public sealed class LegacyFirstRunSetupNativeContractTests
Assert.Equal("SourceRepositoryRoot", metadata.Attribute("Include")?.Value);
Assert.Equal("$(SourceRepositoryRoot)", metadata.Attribute("Value")?.Value);
Assert.Contains(
"'$(LegacyRuntimeAssetsEnabled)' != 'true'",
metadata.Attribute("Condition")?.Value,
StringComparison.Ordinal);
Assert.Contains(
Assert.Equal(
"'$(Configuration)' == 'Debug'",
metadata.Attribute("Condition")?.Value,
StringComparison.Ordinal);
metadata.Attribute("Condition")?.Value);
}
[Fact]

View File

@@ -292,6 +292,226 @@ public sealed class LegacyPlayoutNativeContractTests
AssertEveryFaultBranchQuarantines(loop, "PLAY_CALLBACK_TIMEOUT");
}
[Fact]
public void PendingHelloConfirmationIsNotStoredAsAStickyInitializationError()
{
var connect = Slice(
Playout,
"private async Task ConnectPlayoutAsync(",
"private void OnPlayoutStatusChanged(");
Assert.Contains(
"PlayoutConnectionState.Connecting or",
connect,
StringComparison.Ordinal);
Assert.Contains(
"PlayoutConnectionState.Reconnecting",
connect,
StringComparison.Ordinal);
Assert.Contains(
"_playoutInitializationError = null;",
connect,
StringComparison.Ordinal);
var handler = Slice(
Playout,
"private void OnPlayoutStatusChanged(",
"private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(");
Assert.Contains(
"args.Current.State == PlayoutConnectionState.Connected",
handler,
StringComparison.Ordinal);
Assert.Contains("args.Current.IsConnected", handler, StringComparison.Ordinal);
Assert.Contains(
"_playoutInitializationError = null;",
handler,
StringComparison.Ordinal);
Assert.DoesNotContain("ConnectAsync(", handler, StringComparison.Ordinal);
}
[Fact]
public void CompletionCallbacksReplaceOnlyTheMatchingPendingOperatorMessage()
{
var handler = Slice(
Playout,
"private void OnPlayoutStatusChanged(",
"private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(");
Assert.Contains(
"sender is not IPlayoutEngine statusSource",
handler,
StringComparison.Ordinal);
Assert.Contains(
"!ReferenceEquals(statusSource, _playoutEngine)",
handler,
StringComparison.Ordinal);
Assert.Contains(
"QueuePlayoutStatus(statusSource, args);",
handler,
StringComparison.Ordinal);
Assert.Contains(
"DispatcherQueue.TryEnqueue(() =>",
handler,
StringComparison.Ordinal);
Assert.Contains(
"snapshot.StatusKind == LegacyOperatorStatusKind.Information",
handler,
StringComparison.Ordinal);
Assert.Contains(
"snapshot.StatusMessage,\n expectedPendingMessage,\n" +
" StringComparison.Ordinal",
Normalize(handler),
StringComparison.Ordinal);
Assert.Contains(
"args.Previous.IsTakeOutCompletionPending",
handler,
StringComparison.Ordinal);
Assert.Contains(
"args.Current.State != PlayoutConnectionState.Connected",
handler,
StringComparison.Ordinal);
Assert.Contains("!args.Current.IsConnected", handler, StringComparison.Ordinal);
Assert.Contains(
"!args.Current.IsTakeOutCompletionPending",
handler,
StringComparison.Ordinal);
Assert.Contains(
"args.Previous.IsPlayCompletionPending",
handler,
StringComparison.Ordinal);
Assert.Contains(
"!args.Current.IsPlayCompletionPending",
handler,
StringComparison.Ordinal);
Assert.Contains(
"latestStatus.Sequence >= args.Current.Sequence",
handler,
StringComparison.Ordinal);
Assert.Contains(
"IsPlayoutCompletionStillConfirmed(completionKind, latestStatus)",
handler,
StringComparison.Ordinal);
Assert.Contains(
"PendingTakeOutOperatorMessage",
handler,
StringComparison.Ordinal);
Assert.Contains(
"PendingPlayOperatorMessage",
handler,
StringComparison.Ordinal);
Assert.Contains(
"_controller.ReportInformation(completedMessage)",
handler,
StringComparison.Ordinal);
Assert.Contains(
"CompletedTakeOutOperatorMessage",
handler,
StringComparison.Ordinal);
Assert.Contains(
"CompletedPlayOperatorMessage",
handler,
StringComparison.Ordinal);
var execute = Slice(
Playout,
"private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(",
"private async Task<LegacyOperatorPlayoutRequest> CreateFreshPagedPlayoutRequestAsync(");
var storedPending = RequiredIndex(
execute,
"var information = _controller.ReportInformation(result.Message);");
var reconciled = RequiredIndex(
execute,
"return ReconcileCompletedPlayoutMessage(",
storedPending);
Assert.True(storedPending < reconciled);
var reconcile = Slice(
Playout,
"private LegacyOperatorSnapshot ReconcileCompletedPlayoutMessage(",
"private enum PlayoutCompletionKind");
Assert.Contains(
"IsPlayoutCompletionStillConfirmed(completionKind, statusSource.Status)",
reconcile,
StringComparison.Ordinal);
Assert.Contains(
"PendingTakeOutOperatorMessage",
reconcile,
StringComparison.Ordinal);
Assert.Contains(
"PendingPlayOperatorMessage",
reconcile,
StringComparison.Ordinal);
Assert.Contains(
"CompletedTakeOutOperatorMessage",
reconcile,
StringComparison.Ordinal);
Assert.Contains(
"CompletedPlayOperatorMessage",
reconcile,
StringComparison.Ordinal);
}
[Fact]
public void ConsumedNextIsPersistedForProviderAndEngineFailures()
{
var execute = Slice(
Playout,
"private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(",
"private async Task<LegacyOperatorPlayoutRequest> CreateFreshPagedPlayoutRequestAsync(");
var failedResult = Slice(
execute,
"if (!result.IsSuccess)",
"if (command == LegacyOperatorPlayoutCommand.TakeOut)");
var sceneDataFailure = Slice(
execute,
"catch (LegacySceneDataException exception)",
"catch (Exception exception)");
var consumed = Slice(
Playout,
"private bool TryReportConsumedNext(",
"private async Task<LegacyOperatorPlayoutRequest> CreateFreshPagedPlayoutRequestAsync(");
Assert.Contains(
"result.Code == PlayoutResultCode.Rejected",
failedResult,
StringComparison.Ordinal);
Assert.Contains(
"TryReportConsumedNext(",
failedResult,
StringComparison.Ordinal);
Assert.Contains(
"TryReportConsumedNext(",
sceneDataFailure,
StringComparison.Ordinal);
Assert.Equal(
6,
execute.Split(
"TryReportConsumedNext(",
StringSplitOptions.None).Length - 1);
Assert.Contains(
"var consumedPlaylistRow =",
consumed,
StringComparison.Ordinal);
Assert.Contains(
"cursorPageIndexBefore != workflow.State.PageIndexZeroBased",
consumed,
StringComparison.Ordinal);
Assert.Contains(
"_controller.ReflectSuccessfulPlayout(",
consumed,
StringComparison.Ordinal);
Assert.Contains(
"synchronizeOperatorSelection: false",
consumed,
StringComparison.Ordinal);
Assert.Contains(
"_controller.ReportInformation(",
consumed,
StringComparison.Ordinal);
Assert.Contains(
"return _controller.ReportError(exception.Message);",
sceneDataFailure,
StringComparison.Ordinal);
}
[Fact]
public void ExternalIdleStatusResetsNormalRefreshState()
{
@@ -385,10 +605,45 @@ public sealed class LegacyPlayoutNativeContractTests
Playout,
"private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(",
"private async Task<LegacyOperatorPlayoutRequest> CreateFreshPagedPlayoutRequestAsync(");
var captureNextComposition = RequiredIndex(
execute,
"if (command == LegacyOperatorPlayoutCommand.Next)");
var stopRefresh = RequiredIndex(execute, "StopRefreshLoop();");
var pendingTakeOutGuard = RequiredIndex(
execute,
"if (engine.Status.IsTakeOutCompletionPending)");
var pendingPlayGuard = RequiredIndex(
execute,
"engine.Status.IsPlayCompletionPending)");
var promote = RequiredIndex(execute, "ApplyStagedCompositionForNextCue();");
var dispatch = RequiredIndex(execute, "switch (command)");
Assert.True(stopRefresh < promote && promote < dispatch);
Assert.True(
captureNextComposition < stopRefresh &&
stopRefresh < pendingTakeOutGuard &&
stopRefresh < pendingPlayGuard &&
stopRefresh < promote &&
promote < dispatch);
Assert.Contains(
"workflow.State.IsNextCursorPageNavigationEnabled",
execute,
StringComparison.Ordinal);
Assert.Contains(
"activeCompositionBeforeNext = _compositionOptions!.Current;",
execute,
StringComparison.Ordinal);
Assert.DoesNotContain(
"Keep\n // the current scene's refresh epoch intact",
Normalize(execute),
StringComparison.Ordinal);
Assert.Contains(
"RestoreActiveCompositionAfterFailedNext(",
execute,
StringComparison.Ordinal);
Assert.True(
execute.Split(
"RestoreActiveCompositionAfterFailedNext(",
StringSplitOptions.None).Length - 1 >= 4,
"Known pre-dispatch NEXT failures must restore the previous composition.");
Assert.Contains("LegacyOperatorPlayoutCommand.Prepare or", execute,
StringComparison.Ordinal);
Assert.Contains("LegacyOperatorPlayoutCommand.TakeIn or", execute,

View File

@@ -558,6 +558,10 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
StringComparison.Ordinal);
Assert.Contains("reconnectEnabled = $false", initializer, StringComparison.Ordinal);
Assert.Contains(
"maximumAutomaticRefreshesPerTakeIn = $null",
initializer,
StringComparison.Ordinal);
Assert.DoesNotContain(
"maximumAutomaticRefreshesPerTakeIn = 0",
initializer,
StringComparison.Ordinal);
@@ -830,6 +834,14 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
StringComparison.Ordinal);
Assert.Contains("[switch] $ReplaceLiveConfig", initializer,
StringComparison.Ordinal);
Assert.Contains(
"maximumAutomaticRefreshesPerTakeIn = $null",
initializer,
StringComparison.Ordinal);
Assert.DoesNotContain(
"maximumAutomaticRefreshesPerTakeIn = 0",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"-ConfigureDevelopmentLive requires the default Debug x64 build verification.",
initializer,

View File

@@ -321,7 +321,7 @@ public sealed class LegacyWorkspaceLayoutContractTests
Assert.DoesNotContain("<strong>Tornado2</strong>", header, StringComparison.Ordinal);
Assert.Matches(
@"id=""playout-connection-state""[^>]*role=""status""" +
@"[^>]*aria-label=""Tornado2 연결 상태""[^>]*aria-live=""polite""" +
@"[^>]*aria-label=""송출 연결 상태""[^>]*aria-live=""polite""" +
@"[^>]*aria-atomic=""true"">미연결</div>",
Markup);
Assert.Matches(